# Copyright 2014-2016 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import copy import itertools import os import xml.dom.minidom import xml.etree.ElementTree as ET import uuid import typing as T from pathlib import Path, PurePath import re from . import backends from .. import build from .. import dependencies from .. import mlog from .. import compilers from ..mesonlib import ( File, MesonException, replace_if_different, OptionKey, version_compare, MachineChoice ) from ..environment import Environment, build_filename if T.TYPE_CHECKING: from ..arglist import CompilerArgs from ..interpreter import Interpreter Project = T.Tuple[str, Path, str, MachineChoice] def autodetect_vs_version(build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]) -> backends.Backend: vs_version = os.getenv('VisualStudioVersion', None) vs_install_dir = os.getenv('VSINSTALLDIR', None) if not vs_install_dir: raise MesonException('Could not detect Visual Studio: Environment variable VSINSTALLDIR is not set!\n' 'Are you running meson from the Visual Studio Developer Command Prompt?') # VisualStudioVersion is set since Visual Studio 11.0, but sometimes # vcvarsall.bat doesn't set it, so also use VSINSTALLDIR if vs_version == '11.0' or 'Visual Studio 11' in vs_install_dir: from mesonbuild.backend.vs2012backend import Vs2012Backend return Vs2012Backend(build, interpreter) if vs_version == '12.0' or 'Visual Studio 12' in vs_install_dir: from mesonbuild.backend.vs2013backend import Vs2013Backend return Vs2013Backend(build, interpreter) if vs_version == '14.0' or 'Visual Studio 14' in vs_install_dir: from mesonbuild.backend.vs2015backend import Vs2015Backend return Vs2015Backend(build, interpreter) if vs_version == '15.0' or 'Visual Studio 17' in vs_install_dir or \ 'Visual Studio\\2017' in vs_install_dir: from mesonbuild.backend.vs2017backend import Vs2017Backend return Vs2017Backend(build, interpreter) if vs_version == '16.0' or 'Visual Studio 19' in vs_install_dir or \ 'Visual Studio\\2019' in vs_install_dir: from mesonbuild.backend.vs2019backend import Vs2019Backend return Vs2019Backend(build, interpreter) if vs_version == '17.0' or 'Visual Studio 22' in vs_install_dir or \ 'Visual Studio\\2022' in vs_install_dir: from mesonbuild.backend.vs2022backend import Vs2022Backend return Vs2022Backend(build, interpreter) if 'Visual Studio 10.0' in vs_install_dir: return Vs2010Backend(build, interpreter) raise MesonException('Could not detect Visual Studio using VisualStudioVersion: {!r} or VSINSTALLDIR: {!r}!\n' 'Please specify the exact backend to use.'.format(vs_version, vs_install_dir)) def split_o_flags_args(args: T.List[str]) -> T.List[str]: """ Splits any /O args and returns them. Does not take care of flags overriding previous ones. Skips non-O flag arguments. ['/Ox', '/Ob1'] returns ['/Ox', '/Ob1'] ['/Oxj', '/MP'] returns ['/Ox', '/Oj'] """ o_flags = [] for arg in args: if not arg.startswith('/O'): continue flags = list(arg[2:]) # Assume that this one can't be clumped with the others since it takes # an argument itself if 'b' in flags: o_flags.append(arg) else: 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) class Vs2010Backend(backends.Backend): name = 'vs2010' def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) self.project_file_version = '10.0.30319.1' self.sln_file_version = '11.00' self.sln_version_comment = '2010' self.platform_toolset = None self.vs_version = '2010' self.windows_target_platform_version = None self.subdirs = {} self.handled_target_deps = {} def get_target_private_dir(self, target): return os.path.join(self.get_target_dir(target), target.get_id()) def generate_genlist_for_target(self, genlist: T.Union[build.GeneratedList, build.CustomTarget, build.CustomTargetIndex], target: build.BuildTarget, parent_node: ET.Element, generator_output_files: T.List[str], custom_target_include_dirs: T.List[str], custom_target_output_files: T.List[str]) -> None: if isinstance(genlist, build.GeneratedList): for x in genlist.depends: self.generate_genlist_for_target(x, target, parent_node, [], [], []) target_private_dir = self.relpath(self.get_target_private_dir(target), self.get_target_dir(target)) down = self.target_to_build_root(target) if isinstance(genlist, (build.CustomTarget, build.CustomTargetIndex)): for i in genlist.get_outputs(): # Path to the generated source from the current vcxproj dir via the build root ipath = os.path.join(down, self.get_target_dir(genlist), i) custom_target_output_files.append(ipath) idir = self.relpath(self.get_target_dir(genlist), self.get_target_dir(target)) if idir not in custom_target_include_dirs: custom_target_include_dirs.append(idir) else: generator = genlist.get_generator() exe = generator.get_exe() infilelist = genlist.get_inputs() outfilelist = genlist.get_outputs() source_dir = os.path.join(down, self.build_to_src, genlist.subdir) idgroup = ET.SubElement(parent_node, 'ItemGroup') samelen = len(infilelist) == len(outfilelist) for i, curfile in enumerate(infilelist): if samelen: sole_output = os.path.join(target_private_dir, outfilelist[i]) else: sole_output = '' infilename = os.path.join(down, curfile.rel_to_builddir(self.build_to_src, target_private_dir)) deps = self.get_custom_target_depend_files(genlist, True) base_args = generator.get_arglist(infilename) outfiles_rel = genlist.get_outputs_for(curfile) outfiles = [os.path.join(target_private_dir, of) for of in outfiles_rel] generator_output_files += outfiles args = [x.replace("@INPUT@", infilename).replace('@OUTPUT@', sole_output) for x in base_args] args = self.replace_outputs(args, target_private_dir, outfiles_rel) args = [x.replace("@SOURCE_DIR@", self.environment.get_source_dir()) .replace("@BUILD_DIR@", target_private_dir) for x in args] args = [x.replace("@CURRENT_SOURCE_DIR@", source_dir) for x in args] args = [x.replace("@SOURCE_ROOT@", self.environment.get_source_dir()) .replace("@BUILD_ROOT@", self.environment.get_build_dir()) for x in args] args = [x.replace('\\', '/') for x in args] # Always use a wrapper because MSBuild eats random characters when # there are many arguments. tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) cmd, _ = self.as_meson_exe_cmdline( exe, self.replace_extra_args(args, genlist), workdir=tdir_abs, capture=outfiles[0] if generator.capture else None, force_serialize=True ) deps = cmd[-1:] + deps abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) os.makedirs(abs_pdir, exist_ok=True) cbs = ET.SubElement(idgroup, 'CustomBuild', Include=infilename) ET.SubElement(cbs, 'Command').text = ' '.join(self.quote_arguments(cmd)) ET.SubElement(cbs, 'Outputs').text = ';'.join(outfiles) ET.SubElement(cbs, 'AdditionalInputs').text = ';'.join(deps) def generate_custom_generator_commands(self, target, parent_node): generator_output_files = [] custom_target_include_dirs = [] custom_target_output_files = [] for genlist in target.get_generated_sources(): 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: target_machine = self.interpreter.builtin['target_machine'].cpu_family_method(None, None) if target_machine in {'64', 'x86_64'}: # amd64 or x86_64 target_system = self.interpreter.builtin['target_machine'].system_method(None, None) if detect_microsoft_gdk(target_system): self.platform = target_system else: self.platform = 'x64' elif target_machine == 'x86': # x86 self.platform = 'Win32' elif target_machine in {'aarch64', 'arm64'}: target_cpu = self.interpreter.builtin['target_machine'].cpu_method(None, None) if target_cpu == 'arm64ec': self.platform = 'arm64ec' else: self.platform = 'arm64' elif 'arm' in target_machine.lower(): self.platform = 'ARM' else: raise MesonException('Unsupported Visual Studio platform: ' + target_machine) build_machine = self.interpreter.builtin['build_machine'].cpu_family_method(None, None) if build_machine in {'64', 'x86_64'}: # amd64 or x86_64 self.build_platform = 'x64' elif build_machine == 'x86': # x86 self.build_platform = 'Win32' elif build_machine in {'aarch64', 'arm64'}: target_cpu = self.interpreter.builtin['build_machine'].cpu_method(None, None) if target_cpu == 'arm64ec': self.build_platform = 'arm64ec' else: self.build_platform = 'arm64' elif 'arm' in build_machine.lower(): self.build_platform = 'ARM' else: raise MesonException('Unsupported Visual Studio platform: ' + build_machine) self.buildtype = self.environment.coredata.get_option(OptionKey('buildtype')) self.optimization = self.environment.coredata.get_option(OptionKey('optimization')) self.debug = self.environment.coredata.get_option(OptionKey('debug')) try: self.sanitize = self.environment.coredata.get_option(OptionKey('b_sanitize')) 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')) self.generate_solution(sln_filename, projlist) self.generate_regen_info() Vs2010Backend.touch_regen_timestamp(self.environment.get_build_dir()) @staticmethod def get_regen_stampfile(build_dir: str) -> None: return os.path.join(os.path.join(build_dir, Environment.private_dir), 'regen.stamp') @staticmethod def touch_regen_timestamp(build_dir: str) -> None: with open(Vs2010Backend.get_regen_stampfile(build_dir), 'w', encoding='utf-8'): pass def get_vcvars_command(self): has_arch_values = 'VSCMD_ARG_TGT_ARCH' in os.environ and 'VSCMD_ARG_HOST_ARCH' in os.environ # Use vcvarsall.bat if we found it. if 'VCINSTALLDIR' in os.environ: vs_version = os.environ['VisualStudioVersion'] \ if 'VisualStudioVersion' in os.environ else None relative_path = 'Auxiliary\\Build\\' if vs_version is not None and vs_version >= '15.0' else '' script_path = os.environ['VCINSTALLDIR'] + relative_path + 'vcvarsall.bat' if os.path.exists(script_path): if has_arch_values: target_arch = os.environ['VSCMD_ARG_TGT_ARCH'] host_arch = os.environ['VSCMD_ARG_HOST_ARCH'] else: target_arch = os.environ.get('Platform', 'x86') host_arch = target_arch arch = host_arch + '_' + target_arch if host_arch != target_arch else target_arch return f'"{script_path}" {arch}' # Otherwise try the VS2017 Developer Command Prompt. if 'VS150COMNTOOLS' in os.environ and has_arch_values: script_path = os.environ['VS150COMNTOOLS'] + 'VsDevCmd.bat' if os.path.exists(script_path): return '"%s" -arch=%s -host_arch=%s' % \ (script_path, os.environ['VSCMD_ARG_TGT_ARCH'], os.environ['VSCMD_ARG_HOST_ARCH']) return '' def get_obj_target_deps(self, obj_list): result = {} for o in obj_list: if isinstance(o, build.ExtractedObjects): result[o.target.get_id()] = o.target return result.items() def get_target_deps(self, t: T.Dict[T.Any, build.Target], recursive=False): all_deps: T.Dict[str, build.Target] = {} for target in t.values(): if isinstance(target, build.CustomTarget): for d in target.get_target_dependencies(): # FIXME: this isn't strictly correct, as the target doesn't # Get dependencies on non-targets, such as Files if isinstance(d, build.Target): all_deps[d.get_id()] = d elif isinstance(target, build.RunTarget): for d in target.get_dependencies(): all_deps[d.get_id()] = d elif isinstance(target, build.BuildTarget): for ldep in target.link_targets: if isinstance(ldep, build.CustomTargetIndex): all_deps[ldep.get_id()] = ldep.target else: all_deps[ldep.get_id()] = ldep for ldep in target.link_whole_targets: if isinstance(ldep, build.CustomTargetIndex): all_deps[ldep.get_id()] = ldep.target else: all_deps[ldep.get_id()] = ldep for ldep in target.link_depends: if isinstance(ldep, build.CustomTargetIndex): all_deps[ldep.get_id()] = ldep.target elif isinstance(ldep, File): # Already built, no target references needed pass else: all_deps[ldep.get_id()] = ldep for obj_id, objdep in self.get_obj_target_deps(target.objects): all_deps[obj_id] = objdep else: raise MesonException(f'Unknown target type for target {target}') for gendep in target.get_generated_sources(): if isinstance(gendep, build.CustomTarget): all_deps[gendep.get_id()] = gendep elif isinstance(gendep, build.CustomTargetIndex): all_deps[gendep.target.get_id()] = gendep.target else: generator = gendep.get_generator() gen_exe = generator.get_exe() if isinstance(gen_exe, build.Executable): all_deps[gen_exe.get_id()] = gen_exe for d in itertools.chain(generator.depends, gendep.depends): if isinstance(d, build.CustomTargetIndex): all_deps[d.get_id()] = d.target elif isinstance(d, build.Target): all_deps[d.get_id()] = d # FIXME: we don't handle other kinds of deps correctly here, such # as GeneratedLists, StructuredSources, and generated File. if not t or not recursive: return all_deps ret = self.get_target_deps(all_deps, recursive) ret.update(all_deps) return ret def generate_solution_dirs(self, ofile: str, parents: T.Sequence[Path]) -> None: prj_templ = 'Project("{%s}") = "%s", "%s", "{%s}"\n' iterpaths = reversed(parents) # Skip first path next(iterpaths) for path in iterpaths: if path not in self.subdirs: basename = path.name identifier = generate_guid_from_path(path, 'subdir') # top-level directories have None as their parent_dir parent_dir = path.parent parent_identifier = self.subdirs[parent_dir][0] \ if parent_dir != PurePath('.') else None self.subdirs[path] = (identifier, parent_identifier) prj_line = prj_templ % ( self.environment.coredata.lang_guids['directory'], basename, basename, self.subdirs[path][0]) ofile.write(prj_line) ofile.write('EndProject\n') def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> None: default_projlist = self.get_build_by_default_targets() default_projlist.update(self.get_testlike_targets()) sln_filename_tmp = sln_filename + '~' # Note using the utf-8 BOM requires the blank line, otherwise Visual Studio Version Selector fails. # Without the BOM, VSVS fails if there is a blank line. with open(sln_filename_tmp, 'w', encoding='utf-8-sig') as ofile: ofile.write('\nMicrosoft Visual Studio Solution File, Format Version %s\n' % self.sln_file_version) 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': self.generate_solution_dirs(ofile, prj[1].parents) target = self.build.targets[prj[0]] lang = 'default' if hasattr(target, 'compilers') and target.compilers: for lang_out in target.compilers.keys(): lang = lang_out break prj_line = prj_templ % ( self.environment.coredata.lang_guids[lang], prj[0], prj[1], prj[2]) ofile.write(prj_line) target_dict = {target.get_id(): target} # Get recursive deps recursive_deps = self.get_target_deps( target_dict, recursive=True) ofile.write('EndProject\n') for dep, target in recursive_deps.items(): if prj[0] in default_projlist: default_projlist[dep] = target test_line = prj_templ % (self.environment.coredata.lang_guids['default'], 'RUN_TESTS', 'RUN_TESTS.vcxproj', self.environment.coredata.test_guid) ofile.write(test_line) ofile.write('EndProject\n') regen_line = prj_templ % (self.environment.coredata.lang_guids['default'], 'REGEN', 'REGEN.vcxproj', self.environment.coredata.regen_guid) ofile.write(regen_line) ofile.write('EndProject\n') install_line = prj_templ % (self.environment.coredata.lang_guids['default'], 'RUN_INSTALL', 'RUN_INSTALL.vcxproj', self.environment.coredata.install_guid) ofile.write(install_line) ofile.write('EndProject\n') 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)) 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)) # Create the solution configuration for p in projlist: if p[3] is MachineChoice.BUILD: config_platform = self.build_platform else: config_platform = self.platform # Add to the list of projects in this solution 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)) ofile.write('\tEndGlobalSection\n') ofile.write('\tGlobalSection(SolutionProperties) = preSolution\n') ofile.write('\t\tHideSolutionNode = FALSE\n') ofile.write('\tEndGlobalSection\n') if self.subdirs: ofile.write('\tGlobalSection(NestedProjects) = ' 'preSolution\n') for p in projlist: if p[1].parent != PurePath('.'): ofile.write("\t\t{{{}}} = {{{}}}\n".format(p[2], self.subdirs[p[1].parent][0])) for subdir in self.subdirs.values(): if subdir[1]: ofile.write("\t\t{{{}}} = {{{}}}\n".format(subdir[0], subdir[1])) ofile.write('\tEndGlobalSection\n') ofile.write('EndGlobal\n') replace_if_different(sln_filename, sln_filename_tmp) def generate_projects(self) -> T.List[Project]: startup_project = self.environment.coredata.options[OptionKey('backend_startup_project')].value projlist: T.List[Project] = [] startup_idx = 0 for (i, (name, target)) in enumerate(self.build.targets.items()): if startup_project and startup_project == target.get_basename(): startup_idx = i outdir = Path( self.environment.get_build_dir(), self.get_target_dir(target) ) outdir.mkdir(exist_ok=True, parents=True) fname = name + '.vcxproj' target_dir = PurePath(self.get_target_dir(target)) 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)) # Put the startup project first in the project list if startup_idx: projlist.insert(0, projlist.pop(startup_idx)) return projlist def split_sources(self, srclist): sources = [] headers = [] objects = [] languages = [] for i in srclist: if self.environment.is_header(i): headers.append(i) elif self.environment.is_object(i): objects.append(i) elif self.environment.is_source(i): sources.append(i) lang = self.lang_from_source_file(i) if lang not in languages: languages.append(lang) elif self.environment.is_library(i): pass else: # Everything that is not an object or source file is considered a header. headers.append(i) return sources, headers, objects, languages def target_to_build_root(self, target): if self.get_target_dir(target) == '': return '' directories = os.path.normpath(self.get_target_dir(target)).split(os.sep) return os.sep.join(['..'] * len(directories)) def quote_arguments(self, arr): return ['"%s"' % i for i in arr] def add_project_reference(self, root: ET.Element, include: str, projid: str, link_outputs: bool = False) -> None: ig = ET.SubElement(root, 'ItemGroup') pref = ET.SubElement(ig, 'ProjectReference', Include=include) ET.SubElement(pref, 'Project').text = '{%s}' % projid if not link_outputs: # Do not link in generated .lib files from dependencies automatically. # We only use the dependencies for ordering and link in the generated # objects and .lib files manually. ET.SubElement(pref, 'LinkLibraryDependencies').text = 'false' def add_target_deps(self, root: ET.Element, target): target_dict = {target.get_id(): target} for dep in self.get_target_deps(target_dict).values(): if dep.get_id() in self.handled_target_deps[target.get_id()]: # This dependency was already handled manually. continue relpath = self.get_target_dir_relative_to(dep, target) vcxproj = os.path.join(relpath, dep.get_id() + '.vcxproj') tid = self.environment.coredata.target_guids[dep.get_id()] self.add_project_reference(root, vcxproj, tid) def create_basic_project_filters(self) -> ET.Element: root = ET.Element('Project', {'ToolsVersion': '4.0', 'xmlns': 'http://schemas.microsoft.com/developer/msbuild/2003'}) return root def create_basic_project(self, target_name, *, temp_dir, guid, conftype='Utility', target_ext=None, target_platform=None) -> T.Tuple[ET.Element, ET.Element]: root = ET.Element('Project', {'DefaultTargets': "Build", 'ToolsVersion': '4.0', 'xmlns': 'http://schemas.microsoft.com/developer/msbuild/2003'}) 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 # Globals globalgroup = ET.SubElement(root, 'PropertyGroup', Label='Globals') guidelem = ET.SubElement(globalgroup, 'ProjectGuid') 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 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) 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 return (root, type_config) def gen_run_target_vcxproj(self, target: build.RunTarget, ofname: str, guid: str) -> None: (root, type_config) = self.create_basic_project(target.name, temp_dir=target.get_id(), guid=guid) depend_files = self.get_custom_target_depend_files(target) if not target.command: # This is an alias target and thus doesn't run any command. It's # enough to emit the references to the other projects for them to # be built/run/..., if necessary. assert isinstance(target, build.AliasTarget) assert len(depend_files) == 0 else: assert not isinstance(target, build.AliasTarget) target_env = self.get_run_target_env(target) _, _, cmd_raw = self.eval_custom_target_command(target) wrapper_cmd, _ = self.as_meson_exe_cmdline(target.command[0], cmd_raw[1:], force_serialize=True, env=target_env, verbose=True) self.add_custom_build(root, 'run_target', ' '.join(self.quote_arguments(wrapper_cmd)), deps=depend_files) # The import is needed even for alias targets, otherwise the build # target isn't defined ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) def gen_custom_target_vcxproj(self, target: build.CustomTarget, ofname: str, guid: str) -> None: if target.for_machine is MachineChoice.BUILD: platform = self.build_platform else: platform = self.platform (root, type_config) = self.create_basic_project(target.name, temp_dir=target.get_id(), guid=guid, target_platform=platform) # We need to always use absolute paths because our invocation is always # from the target dir, not the build root. target.absolute_paths = True (srcs, ofilenames, cmd) = self.eval_custom_target_command(target, True) depend_files = self.get_custom_target_depend_files(target, True) # Always use a wrapper because MSBuild eats random characters when # there are many arguments. tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) extra_bdeps = target.get_transitive_build_target_deps() wrapper_cmd, _ = self.as_meson_exe_cmdline(target.command[0], cmd[1:], # All targets run from the target dir workdir=tdir_abs, extra_bdeps=extra_bdeps, capture=ofilenames[0] if target.capture else None, feed=srcs[0] if target.feed else None, force_serialize=True, env=target.env, verbose=target.console) if target.build_always_stale: # Use a nonexistent file to always consider the target out-of-date. ofilenames += [self.nonexistent_file(os.path.join(self.environment.get_scratch_dir(), 'outofdate.file'))] self.add_custom_build(root, 'custom_target', ' '.join(self.quote_arguments(wrapper_cmd)), deps=wrapper_cmd[-1:] + srcs + depend_files, outputs=ofilenames, verify_files=not target.build_always_stale) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.generate_custom_generator_commands(target, root) self.add_regen_dependency(root) self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) def gen_compile_target_vcxproj(self, target: build.CompileTarget, ofname: str, guid: str) -> None: if target.for_machine is MachineChoice.BUILD: platform = self.build_platform else: platform = self.platform (root, type_config) = self.create_basic_project(target.name, temp_dir=target.get_id(), guid=guid, target_platform=platform) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') target.generated = [self.compile_target_to_generator(target)] target.sources = [] self.generate_custom_generator_commands(target, root) self.add_regen_dependency(root) self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) @classmethod def lang_from_source_file(cls, src): ext = src.split('.')[-1] if ext in compilers.c_suffixes: return 'c' if ext in compilers.cpp_suffixes: return 'cpp' raise MesonException(f'Could not guess language from source file {src}.') def add_pch(self, pch_sources, lang, inc_cl): if lang in pch_sources: self.use_pch(pch_sources, lang, inc_cl) def create_pch(self, pch_sources, lang, inc_cl): pch = ET.SubElement(inc_cl, 'PrecompiledHeader') pch.text = 'Create' self.add_pch_files(pch_sources, lang, inc_cl) def use_pch(self, pch_sources, lang, inc_cl): pch = ET.SubElement(inc_cl, 'PrecompiledHeader') pch.text = 'Use' header = self.add_pch_files(pch_sources, lang, inc_cl) pch_include = ET.SubElement(inc_cl, 'ForcedIncludeFiles') pch_include.text = header + ';%(ForcedIncludeFiles)' def add_pch_files(self, pch_sources, lang, inc_cl): header = os.path.basename(pch_sources[lang][0]) pch_file = ET.SubElement(inc_cl, 'PrecompiledHeaderFile') # When USING PCHs, MSVC will not do the regular include # directory lookup, but simply use a string match to find the # PCH to use. That means the #include directive must match the # pch_file.text used during PCH CREATION verbatim. # When CREATING a PCH, MSVC will do the include directory # lookup to find the actual PCH header to use. Thus, the PCH # header must either be in the include_directories of the target # or be in the same directory as the PCH implementation. pch_file.text = header pch_out = ET.SubElement(inc_cl, 'PrecompiledHeaderOutputFile') pch_out.text = f'$(IntDir)$(TargetName)-{lang}.pch' # Need to set the name for the pdb, as cl otherwise gives it a static # name. Which leads to problems when there is more than one pch # (e.g. for different languages). pch_pdb = ET.SubElement(inc_cl, 'ProgramDataBaseFileName') pch_pdb.text = f'$(IntDir)$(TargetName)-{lang}.pdb' return header def is_argument_with_msbuild_xml_entry(self, entry): # Remove arguments that have a top level XML entry so # they are not used twice. # FIXME add args as needed. if entry[1:].startswith('fsanitize'): return True return entry[1:].startswith('M') def add_additional_options(self, lang, parent_node, file_args): args = [] for arg in file_args[lang].to_native(): if self.is_argument_with_msbuild_xml_entry(arg): continue if arg == '%(AdditionalOptions)': args.append(arg) else: args.append(self.escape_additional_option(arg)) ET.SubElement(parent_node, "AdditionalOptions").text = ' '.join(args) def add_preprocessor_defines(self, lang, parent_node, file_defines): defines = [] for define in file_defines[lang]: if define == '%(PreprocessorDefinitions)': defines.append(define) else: defines.append(self.escape_preprocessor_define(define)) ET.SubElement(parent_node, "PreprocessorDefinitions").text = ';'.join(defines) def add_include_dirs(self, lang, parent_node, file_inc_dirs): dirs = file_inc_dirs[lang] ET.SubElement(parent_node, "AdditionalIncludeDirectories").text = ';'.join(dirs) @staticmethod def has_objects(objects, additional_objects, generated_objects): # Ignore generated objects, those are automatically used by MSBuild because they are part of # the CustomBuild Outputs. return len(objects) + len(additional_objects) > 0 @staticmethod def add_generated_objects(node, generated_objects): # Do not add generated objects to project file. Those are automatically used by MSBuild, because # they are part of the CustomBuild Outputs. return @staticmethod def escape_preprocessor_define(define: str) -> str: # See: https://msdn.microsoft.com/en-us/library/bb383819.aspx table = str.maketrans({'%': '%25', '$': '%24', '@': '%40', "'": '%27', ';': '%3B', '?': '%3F', '*': '%2A', # We need to escape backslash because it'll be un-escaped by # Windows during process creation when it parses the arguments # Basically, this converts `\` to `\\`. '\\': '\\\\'}) return define.translate(table) @staticmethod def escape_additional_option(option: str) -> str: # See: https://msdn.microsoft.com/en-us/library/bb383819.aspx table = str.maketrans({'%': '%25', '$': '%24', '@': '%40', "'": '%27', ';': '%3B', '?': '%3F', '*': '%2A', ' ': '%20'}) option = option.translate(table) # Since we're surrounding the option with ", if it ends in \ that will # escape the " when the process arguments are parsed and the starting # " will not terminate. So we escape it if that's the case. I'm not # kidding, this is how escaping works for process args on Windows. if option.endswith('\\'): option += '\\' return f'"{option}"' @staticmethod def add_filter_info(list_filters_path, filter_group, sub_element, file_path, forced_filter_name=None, down=''): filter_inc_cl = ET.SubElement(filter_group, sub_element, Include=file_path) # Force the subdir if forced_filter_name: filter_path = forced_filter_name else: # Create a subdir following the placement if on the same drive filter_path = Path(file_path).resolve().parent if Path(file_path).drive == Path(down).drive: filter_path = Path(os.path.relpath(str(filter_path), down)).as_posix().replace('../', '').replace('..', '') else: return # No filter needed # Needed to have non posix path filter_path = filter_path.replace('/', '\\') if filter_path and filter_path != '.': # Remove ending backslash filter_path = filter_path.rstrip('\\') # Create a hierarchical level of directories list_path = filter_path.split('\\') new_filter_path = '' for path in list_path: if new_filter_path: new_filter_path = new_filter_path + '\\' + path else: new_filter_path = path list_filters_path.add(new_filter_path) # Create a new filter node for the current file added ET.SubElement(filter_inc_cl, 'Filter').text = filter_path @staticmethod def split_link_args(args): """ Split a list of link arguments into three lists: * library search paths * library filenames (or paths) * other link arguments """ lpaths = [] libs = [] other = [] for arg in args: if arg.startswith('/LIBPATH:'): lpath = arg[9:] # De-dup library search paths by removing older entries when # a new one is found. This is necessary because unlike other # search paths such as the include path, the library is # searched for in the newest (right-most) search path first. if lpath in lpaths: lpaths.remove(lpath) lpaths.append(lpath) elif arg.startswith(('/', '-')): other.append(arg) # It's ok if we miss libraries with non-standard extensions here. # They will go into the general link arguments. elif arg.endswith('.lib') or arg.endswith('.a'): # De-dup if arg not in libs: libs.append(arg) else: other.append(arg) return lpaths, libs, other def _get_cl_compiler(self, target): for lang, c in target.compilers.items(): if lang in {'c', 'cpp'}: return c # No source files, only objects, but we still need a compiler, so # return a found compiler if len(target.objects) > 0: for lang, c in self.environment.coredata.compilers[target.for_machine].items(): if lang in {'c', 'cpp'}: return c raise MesonException('Could not find a C or C++ compiler. MSVC can only build C/C++ projects.') def _prettyprint_vcxproj_xml(self, tree: ET.ElementTree, ofname: str) -> None: ofname_tmp = ofname + '~' tree.write(ofname_tmp, encoding='utf-8', xml_declaration=True) # ElementTree cannot do pretty-printing, so do it manually doc = xml.dom.minidom.parse(ofname_tmp) with open(ofname_tmp, 'w', encoding='utf-8') as of: 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()}') # 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 # Arguments, include dirs, defines for all files in the current target target_args = [] target_defines = [] target_inc_dirs = [] # Arguments, include dirs, defines passed to individual files in # a target; perhaps because the args are language-specific # # file_args is also later split out into defines and include_dirs in # case someone passed those in there file_args: T.Dict[str, CompilerArgs] = {l: c.compiler_args() for l, c in target.compilers.items()} file_defines = {l: [] for l in target.compilers} file_inc_dirs = {l: [] for l in target.compilers} # The order in which these compile args are added must match # generate_single_compile() and generate_basic_compiler_args() for l, comp in target.compilers.items(): if l in file_args: file_args[l] += compilers.get_base_compile_args( target.get_options(), comp) file_args[l] += comp.get_option_compile_args( target.get_options()) # Add compile args added using add_project_arguments() for l, args in self.build.projects_args[target.for_machine].get(target.subproject, {}).items(): if l in file_args: file_args[l] += args # Add compile args added using add_global_arguments() # These override per-project arguments for l, args in self.build.global_args[target.for_machine].items(): if l in file_args: file_args[l] += args # Compile args added from the env or cross file: CFLAGS/CXXFLAGS, etc. We want these # to override all the defaults, but not the per-target compile args. for l in file_args.keys(): opts = self.environment.coredata.options[OptionKey('args', machine=target.for_machine, lang=l)] file_args[l] += opts.value for args in file_args.values(): # This is where Visual Studio will insert target_args, target_defines, # etc, which are added later from external deps (see below). args += ['%(AdditionalOptions)', '%(PreprocessorDefinitions)', '%(AdditionalIncludeDirectories)'] # Add custom target dirs as includes automatically, but before # target-specific include dirs. See _generate_single_compile() in # the ninja backend for caveats. args += ['-I' + arg for arg in generated_files_include_dirs] # Add include dirs from the `include_directories:` kwarg on the target # and from `include_directories:` of internal deps of the target. # # Target include dirs should override internal deps include dirs. # This is handled in BuildTarget.process_kwargs() # # Include dirs from internal deps should override include dirs from # external deps and must maintain the order in which they are # specified. Hence, we must reverse so that the order is preserved. # # These are per-target, but we still add them as per-file because we # need them to be looked in first. for d in reversed(target.get_include_dirs()): # reversed is used to keep order of includes for i in reversed(d.get_incdirs()): curdir = os.path.join(d.get_curdir(), i) try: # Add source subdir first so that the build subdir overrides it args.append('-I' + os.path.join(proj_to_src_root, curdir)) # src dir args.append('-I' + self.relpath(curdir, target.subdir)) # build dir except ValueError: # Include is on different drive args.append('-I' + os.path.normpath(curdir)) for i in d.get_extra_build_dirs(): curdir = os.path.join(d.get_curdir(), i) args.append('-I' + self.relpath(curdir, target.subdir)) # build dir # Add per-target compile args, f.ex, `c_args : ['/DFOO']`. We set these # near the end since these are supposed to override everything else. for l, args in target.extra_args.items(): if l in file_args: file_args[l] += args # The highest priority includes. In order of directory search: # target private dir, target build dir, target source dir for args in file_args.values(): t_inc_dirs = [self.relpath(self.get_target_private_dir(target), self.get_target_dir(target))] if target.implicit_include_directories: t_inc_dirs += ['.', proj_to_src_dir] args += ['-I' + arg for arg in t_inc_dirs] # Split preprocessor defines and include directories out of the list of # all extra arguments. The rest go into %(AdditionalOptions). for l, args in file_args.items(): for arg in args[:]: if arg.startswith(('-D', '/D')) or arg == '%(PreprocessorDefinitions)': file_args[l].remove(arg) # Don't escape the marker if arg == '%(PreprocessorDefinitions)': define = arg else: define = arg[2:] # De-dup if define not in file_defines[l]: file_defines[l].append(define) elif arg.startswith(('-I', '/I')) or arg == '%(AdditionalIncludeDirectories)': file_args[l].remove(arg) # Don't escape the marker if arg == '%(AdditionalIncludeDirectories)': inc_dir = arg else: inc_dir = arg[2:] # De-dup if inc_dir not in file_inc_dirs[l]: file_inc_dirs[l].append(inc_dir) # Add include dirs to target as well so that "Go to Document" works in headers if inc_dir not in target_inc_dirs: target_inc_dirs.append(inc_dir) # Split compile args needed to find external dependencies # Link args are added while generating the link command 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 isinstance(d, dependencies.OpenMPDependency): ET.SubElement(clconf, 'OpenMPSupport').text = 'true' else: d_compile_args = compiler.unix_args_to_native(d.get_compile_args()) for arg in d_compile_args: if arg.startswith(('-D', '/D')): define = arg[2:] # De-dup if define in target_defines: target_defines.remove(define) target_defines.append(define) elif arg.startswith(('-I', '/I')): inc_dir = arg[2:] # De-dup if inc_dir not in target_inc_dirs: target_inc_dirs.append(inc_dir) else: target_args.append(arg) languages += gen_langs if '/Gw' in build_args: target_args.append('/Gw') if len(target_args) > 0: target_args.append('%(AdditionalOptions)') ET.SubElement(clconf, "AdditionalOptions").text = ' '.join(target_args) ET.SubElement(clconf, 'AdditionalIncludeDirectories').text = ';'.join(target_inc_dirs) target_defines.append('%(PreprocessorDefinitions)') ET.SubElement(clconf, 'PreprocessorDefinitions').text = ';'.join(target_defines) ET.SubElement(clconf, 'FunctionLevelLinking').text = 'true' # Warning level warning_level = T.cast('str', target.get_option(OptionKey('warning_level'))) ET.SubElement(clconf, 'WarningLevel').text = 'Level' + str(1 + int(warning_level)) if target.get_option(OptionKey('werror')): ET.SubElement(clconf, 'TreatWarningAsError').text = 'true' # Optimization flags o_flags = split_o_flags_args(build_args) if '/Ox' in o_flags: ET.SubElement(clconf, 'Optimization').text = 'Full' elif '/O2' in o_flags: ET.SubElement(clconf, 'Optimization').text = 'MaxSpeed' elif '/O1' in o_flags: ET.SubElement(clconf, 'Optimization').text = 'MinSpace' elif '/Od' in o_flags: ET.SubElement(clconf, 'Optimization').text = 'Disabled' if '/Oi' in o_flags: ET.SubElement(clconf, 'IntrinsicFunctions').text = 'true' if '/Ob1' in o_flags: ET.SubElement(clconf, 'InlineFunctionExpansion').text = 'OnlyExplicitInline' elif '/Ob2' in o_flags: ET.SubElement(clconf, 'InlineFunctionExpansion').text = 'AnySuitable' # Size-preserving flags if '/Os' in o_flags: ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Size' # Note: setting FavorSizeOrSpeed with clang-cl conflicts with /Od and can make debugging difficult, so don't. elif '/Od' not in o_flags: ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Speed' # Note: SuppressStartupBanner is /NOLOGO and is 'true' by default self.generate_lang_standard_info(file_args, clconf) 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') # Linker options link = ET.SubElement(compiles, 'Link') extra_link_args = compiler.compiler_args() # FIXME: Can these buildtype linker args be added as tags in the # vcxproj file (similar to buildtype compiler args) instead of in # AdditionalOptions? extra_link_args += compiler.get_buildtype_linker_args(self.buildtype) # Generate Debug info if self.debug: self.generate_debug_information(link) else: ET.SubElement(link, 'GenerateDebugInformation').text = 'false' if not isinstance(target, build.StaticLibrary): if isinstance(target, build.SharedModule): options = self.environment.coredata.options extra_link_args += compiler.get_std_shared_module_link_args(options) # Add link args added using add_project_link_arguments() extra_link_args += self.build.get_project_link_args(compiler, target.subproject, target.for_machine) # Add link args added using add_global_link_arguments() # These override per-project link arguments extra_link_args += self.build.get_global_link_args(compiler, target.for_machine) # Link args added from the env: LDFLAGS, or the cross file. We want # these to override all the defaults but not the per-target link # args. extra_link_args += self.environment.coredata.get_external_link_args( target.for_machine, compiler.get_language()) # Only non-static built targets need link args and link dependencies extra_link_args += target.link_args # External deps must be last because target link libraries may depend on them. for dep in target.get_external_deps(): # Extend without reordering or de-dup to preserve `-L -l` sets # https://github.com/mesonbuild/meson/issues/1718 if isinstance(dep, dependencies.OpenMPDependency): ET.SubElement(clconf, 'OpenMPSupport').text = 'true' else: extra_link_args.extend_direct(dep.get_link_args()) for d in target.get_dependencies(): if isinstance(d, build.StaticLibrary): for dep in d.get_external_deps(): if isinstance(dep, dependencies.OpenMPDependency): ET.SubElement(clconf, 'OpenMPSupport').text = 'true' else: extra_link_args.extend_direct(dep.get_link_args()) # Add link args for c_* or cpp_* build options. Currently this only # adds c_winlibs and cpp_winlibs when building for Windows. This needs # to be after all internal and external libraries so that unresolved # symbols from those can be found here. This is needed when the # *_winlibs that we want to link to are static mingw64 libraries. extra_link_args += compiler.get_option_link_args(self.environment.coredata.options) (additional_libpaths, additional_links, extra_link_args) = self.split_link_args(extra_link_args.to_native()) # Add more libraries to be linked if needed for t in target.get_dependencies(): if isinstance(t, build.CustomTargetIndex): # We don't need the actual project here, just the library name lobj = t else: lobj = self.build.targets[t.get_id()] linkname = os.path.join(down, self.get_target_filename_for_linking(lobj)) if t in target.link_whole_targets: if compiler.id == 'msvc' and version_compare(compiler.version, '<19.00.23918'): # Expand our object lists manually if we are on pre-Visual Studio 2015 Update 2 l = t.extract_all_objects(False) # Unfortunately, we can't use self.object_filename_from_source() for gen in l.genlist: for src in gen.get_outputs(): if self.environment.is_source(src): path = self.get_target_generated_dir(t, gen, src) gen_src_ext = '.' + os.path.splitext(path)[1][1:] extra_link_args.append(path[:-len(gen_src_ext)] + '.obj') for src in l.srclist: obj_basename = None if self.environment.is_source(src): obj_basename = self.object_filename_from_source(t, src) target_private_dir = self.relpath(self.get_target_private_dir(t), self.get_target_dir(t)) rel_obj = os.path.join(target_private_dir, obj_basename) extra_link_args.append(rel_obj) extra_link_args.extend(self.flatten_object_list(t)) else: # /WHOLEARCHIVE:foo must go into AdditionalOptions extra_link_args += compiler.get_link_whole_for(linkname) # To force Visual Studio to build this project even though it # has no sources, we include a reference to the vcxproj file # that builds this target. Technically we should add this only # if the current target has no sources, but it doesn't hurt to # have 'extra' references. trelpath = self.get_target_dir_relative_to(t, target) tvcxproj = os.path.join(trelpath, t.get_id() + '.vcxproj') tid = self.environment.coredata.target_guids[t.get_id()] self.add_project_reference(root, tvcxproj, tid, link_outputs=True) # Mark the dependency as already handled to not have # multiple references to the same target. self.handled_target_deps[target.get_id()].append(t.get_id()) else: # Other libraries go into AdditionalDependencies if linkname not in additional_links: 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)') ET.SubElement(link, "AdditionalOptions").text = ' '.join(extra_link_args) if len(additional_libpaths) > 0: additional_libpaths.insert(0, '%(AdditionalLibraryDirectories)') ET.SubElement(link, 'AdditionalLibraryDirectories').text = ';'.join(additional_libpaths) if len(additional_links) > 0: additional_links.append('%(AdditionalDependencies)') ET.SubElement(link, 'AdditionalDependencies').text = ';'.join(additional_links) ofile = ET.SubElement(link, 'OutputFile') ofile.text = f'$(OutDir){target.get_filename()}' subsys = ET.SubElement(link, 'SubSystem') subsys.text = subsystem if isinstance(target, (build.SharedLibrary, build.Executable)) and target.get_import_filename(): # DLLs built with MSVC always have an import library except when # they're data-only DLLs, but we don't support those yet. ET.SubElement(link, 'ImportLibrary').text = target.get_import_filename() if isinstance(target, build.SharedLibrary): # Add module definitions file, if provided if target.vs_module_defs: relpath = os.path.join(down, target.vs_module_defs.rel_to_builddir(self.build_to_src)) ET.SubElement(link, 'ModuleDefinitionFile').text = relpath if self.debug: pdb = ET.SubElement(link, 'ProgramDataBaseFileName') pdb.text = f'$(OutDir){target_name}.pdb' targetmachine = ET.SubElement(link, 'TargetMachine') if target.for_machine is MachineChoice.BUILD: targetplatform = platform.lower() else: targetplatform = self.platform.lower() if targetplatform == 'win32': targetmachine.text = 'MachineX86' elif targetplatform == 'x64' or detect_microsoft_gdk(targetplatform): targetmachine.text = 'MachineX64' elif targetplatform == 'arm': targetmachine.text = 'MachineARM' elif targetplatform == 'arm64': targetmachine.text = 'MachineARM64' elif targetplatform == 'arm64ec': targetmachine.text = 'MachineARM64EC' else: raise MesonException('Unsupported Visual Studio target machine: ' + targetplatform) # /nologo ET.SubElement(link, 'SuppressStartupBanner').text = 'true' # /release if not self.environment.coredata.get_option(OptionKey('debug')): ET.SubElement(link, 'SetChecksum').text = 'true' meson_file_group = ET.SubElement(root, 'ItemGroup') ET.SubElement(meson_file_group, 'None', Include=os.path.join(proj_to_src_dir, build_filename)) # Visual Studio can't load projects that present duplicated items. Filter them out # by keeping track of already added paths. def path_normalize_add(path, lis): normalized = os.path.normcase(os.path.normpath(path)) if normalized not in lis: lis.append(normalized) return True else: return False list_filters_path = set() previous_includes = [] if len(headers) + len(gen_hdrs) + len(target.extra_files) + len(pch_sources) > 0: # 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)) 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) for h in gen_hdrs: if path_normalize_add(h, previous_includes): 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)) 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) for headers in pch_sources.values(): path = os.path.join(proj_to_src_dir, headers[0]) if path_normalize_add(path, previous_includes): self.add_filter_info(list_filters_path, filter_group_include, 'ClInclude', path, 'pch') ET.SubElement(inc_hdrs, 'CLInclude', Include=path) previous_sources = [] if len(sources) + len(gen_src) + len(pch_sources) > 0: # 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)) 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) 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) 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] 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') for filter_dir in list_filters_path: filter = ET.SubElement(filter_group, 'Filter', Include=filter_dir) ET.SubElement(filter, 'UniqueIdentifier').text = '{' + str(uuid.uuid4()) + '}' 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)) if path_normalize_add(relpath, previous_objects): ET.SubElement(inc_objs, 'Object', Include=relpath) for s in additional_objects: if path_normalize_add(s, previous_objects): ET.SubElement(inc_objs, 'Object', Include=s) self.add_generated_objects(inc_objs, gen_objs) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) self._prettyprint_vcxproj_xml(ET.ElementTree(root_filter), ofname + '.filters') 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 "%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.') 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): guid = self.environment.coredata.test_guid (root, type_config) = self.create_basic_project(target_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))) 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() guid = self.environment.coredata.install_guid (root, type_config) = self.create_basic_project(target_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))) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) def add_custom_build(self, node: ET.Element, rulename: str, command: str, deps: T.Optional[T.List[str]] = None, outputs: T.Optional[T.List[str]] = None, msg: T.Optional[str] = None, verify_files: bool = True) -> None: igroup = ET.SubElement(node, 'ItemGroup') rulefile = os.path.join(self.environment.get_scratch_dir(), rulename + '.rule') if not os.path.exists(rulefile): with open(rulefile, 'w', encoding='utf-8') as f: f.write("# Meson regen file.") custombuild = ET.SubElement(igroup, 'CustomBuild', Include=rulefile) if msg: message = ET.SubElement(custombuild, 'Message') message.text = msg if not verify_files: ET.SubElement(custombuild, 'VerifyInputsAndOutputsExist').text = 'false' # If a command ever were to change the current directory or set local # variables this would need to be more complicated, as msbuild by # default executes all CustomBuilds in a project using the same # shell. Right now such tasks are all done inside the meson_exe # wrapper. The trailing newline appears to be necessary to allow # parallel custom builds to work. ET.SubElement(custombuild, 'Command').text = f"{command}\n" if not outputs: # Use a nonexistent file to always consider the target out-of-date. outputs = [self.nonexistent_file(os.path.join(self.environment.get_scratch_dir(), 'outofdate.file'))] ET.SubElement(custombuild, 'Outputs').text = ';'.join(outputs) if deps: ET.SubElement(custombuild, 'AdditionalInputs').text = ';'.join(deps) @staticmethod def nonexistent_file(prefix: str) -> str: i = 0 file = prefix while os.path.exists(file): file = '%s%d' % (prefix, i) return file def generate_debug_information(self, link: ET.Element) -> None: # valid values for vs2015 is 'false', 'true', 'DebugFastLink' 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) def generate_lang_standard_info(self, file_args: T.Dict[str, CompilerArgs], clconf: ET.Element) -> None: pass