Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tools msbuilddeps visitor improved #8733

199 changes: 105 additions & 94 deletions conan/tools/microsoft/msbuilddeps.py
Expand Up @@ -14,40 +14,42 @@

class MSBuildDeps(object):

_vars_conf_props = textwrap.dedent("""\
_vars_props = textwrap.dedent("""\
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="ConanVariables">
<Conan{name}RootFolder>{root_folder}</Conan{name}RootFolder>
<Conan{name}CompilerFlags>{compiler_flags}</Conan{name}CompilerFlags>
<Conan{name}LinkerFlags>{linker_flags}</Conan{name}LinkerFlags>
<Conan{name}PreprocessorDefinitions>{definitions}</Conan{name}PreprocessorDefinitions>
<Conan{name}IncludeDirectories>{include_dirs}</Conan{name}IncludeDirectories>
<Conan{name}ResourceDirectories>{res_dirs}</Conan{name}ResourceDirectories>
<Conan{name}LibraryDirectories>{lib_dirs}</Conan{name}LibraryDirectories>
<Conan{name}BinaryDirectories>{bin_dirs}</Conan{name}BinaryDirectories>
<Conan{name}Libraries>{libs}</Conan{name}Libraries>
<Conan{name}SystemDeps>{system_libs}</Conan{name}SystemDeps>
<Conan{{name}}RootFolder>{{root_folder}}</Conan{{name}}RootFolder>
<Conan{{name}}CompilerFlags>{{compiler_flags}}</Conan{{name}}CompilerFlags>
<Conan{{name}}LinkerFlags>{{linker_flags}}</Conan{{name}}LinkerFlags>
<Conan{{name}}PreprocessorDefinitions>{{definitions}}</Conan{{name}}PreprocessorDefinitions>
<Conan{{name}}IncludeDirectories>{{include_dirs}}</Conan{{name}}IncludeDirectories>
<Conan{{name}}ResourceDirectories>{{res_dirs}}</Conan{{name}}ResourceDirectories>
<Conan{{name}}LibraryDirectories>{{lib_dirs}}</Conan{{name}}LibraryDirectories>
<Conan{{name}}BinaryDirectories>{{bin_dirs}}</Conan{{name}}BinaryDirectories>
<Conan{{name}}Libraries>{{libs}}</Conan{{name}}Libraries>
<Conan{{name}}SystemLibs>{{system_libs}}</Conan{{name}}SystemLibs>
<Conan{{name}}Dependencies>{{dependencies}}</Conan{{name}}Dependencies>
</PropertyGroup>
</Project>
""")

_dep_props = textwrap.dedent("""\
_conf_props = textwrap.dedent("""\
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="ConanDependencies">
{% for dep in deps %}
<Import Condition="'$(conan_{{dep}}_props_imported)' != 'True'" Project="conan_{{dep}}.props"/>
{% endfor %}
</ImportGroup>
<ImportGroup Label="Configurations">
<ImportGroup Label="ConanPackageVariables">
<Import Project="{{vars_filename}}"/>
</ImportGroup>
<PropertyGroup>
<conan_{{name}}_props_imported>True</conan_{{name}}_props_imported>
</PropertyGroup>
<PropertyGroup>
<LocalDebuggerEnvironment>PATH=%PATH%;$(Conan{{name}}BinaryDirectories)$(LocalDebuggerEnvironment)</LocalDebuggerEnvironment>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
{% if ca_exclude -%}
{% if ca_exclude %}
<CAExcludePath>$(Conan{{name}}IncludeDirectories);$(CAExcludePath)</CAExcludePath>
{%- endif %}
{% endif %}
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
Expand All @@ -58,7 +60,7 @@ class MSBuildDeps(object):
<Link>
<AdditionalLibraryDirectories>$(Conan{{name}}LibraryDirectories)%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(Conan{{name}}Libraries)%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(Conan{{name}}SystemDeps)%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(Conan{{name}}SystemLibs)%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalOptions>$(Conan{{name}}LinkerFlags) %(AdditionalOptions)</AdditionalOptions>
</Link>
<Midl>
Expand All @@ -73,6 +75,25 @@ class MSBuildDeps(object):
</Project>
""")

_dep_props = textwrap.dedent("""\
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="Configurations">
</ImportGroup>
<PropertyGroup>
<conan_{{name}}_props_imported>True</conan_{{name}}_props_imported>
</PropertyGroup>
</Project>
""")

_all_props = textwrap.dedent("""\
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="ConanDependencies">
</ImportGroup>
</Project>
""")

def __init__(self, conanfile):
self._conanfile = conanfile
self.configuration = conanfile.settings.build_type
Expand Down Expand Up @@ -121,47 +142,10 @@ def _condition(self):
condition = " And ".join("'$(%s)' == '%s'" % (k, v) for k, v in props)
return condition

def _deps_props(self, name_general, deps):
""" this is a .props file including all declared dependencies
def _vars_props_file(self, name, cpp_info, deps):
"""
content for conan_vars_poco_x86_release.props, containing the variables
"""
# read the existing multi_filename or use the template if it doesn't exist
template = textwrap.dedent("""\
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="ConanDependencies" >
</ImportGroup>
</Project>
""")
multi_path = os.path.join(self.output_path, name_general)
if os.path.isfile(multi_path):
content_multi = load(multi_path)
else:
content_multi = template

# parse the multi_file and add a new import statement if needed
dom = minidom.parseString(content_multi)
import_group = dom.getElementsByTagName('ImportGroup')[0]
children = import_group.getElementsByTagName("Import")
for dep in deps:
conf_props_name = "conan_%s.props" % dep.name
for node in children:
if conf_props_name == node.getAttribute("Project"):
# the import statement already exists
break
else:
# create a new import statement
import_node = dom.createElement('Import')
dep_imported = "'$(conan_%s_props_imported)' != 'True'" % dep.name
import_node.setAttribute('Project', conf_props_name)
import_node.setAttribute('Condition', dep_imported)
# add it to the import group
import_group.appendChild(import_node)
content_multi = dom.toprettyxml()
# To remove all extra blank lines
content_multi = "\n".join(line for line in content_multi.splitlines() if line.strip())
return content_multi

def _pkg_config_props(self, name, cpp_info):
# returns a .props file with the variables definition for one package for one configuration
def add_valid_ext(libname):
ext = os.path.splitext(libname)[1]
Expand All @@ -180,18 +164,15 @@ def add_valid_ext(libname):
'compiler_flags': " ".join(cpp_info.cxxflags + cpp_info.cflags),
'linker_flags': " ".join(cpp_info.sharedlinkflags),
'exe_flags': " ".join(cpp_info.exelinkflags),
'dependencies': ";".join(deps)
}
formatted_template = self._vars_conf_props.format(**fields)
formatted_template = Template(self._vars_props).render(**fields)
return formatted_template

def _pkg_props(self, name_multi, dep_name, vars_props_name, condition, cpp_info):
# read the existing mult_filename or use the template if it doesn't exist
multi_path = os.path.join(self.output_path, name_multi)
if os.path.isfile(multi_path):
content_multi = load(multi_path)
else:
content_multi = self._dep_props

def _conf_props_file(self, dep_name, vars_props_name, deps):
"""
content for conan_poco_x86_release.props, containing the activation
"""
# TODO: This must include somehow the user/channel, most likely pattern to exclude/include
# Probably also the negation pattern, exclude all not @mycompany/*
ca_exclude = False
Expand All @@ -203,43 +184,71 @@ def _pkg_props(self, name_multi, dep_name, vars_props_name, condition, cpp_info)
else:
ca_exclude = self.exclude_code_analysis

content_multi = Template(content_multi).render(name=dep_name, ca_exclude=ca_exclude)
template = Template(self._conf_props, trim_blocks=True, lstrip_blocks=True)
content_multi = template.render(name=dep_name, ca_exclude=ca_exclude,
vars_filename=vars_props_name, deps=deps)
return content_multi

def _dep_props_file(self, name, name_general, dep_props_filename, condition):
multi_path = os.path.join(self.output_path, name_general)
if os.path.isfile(multi_path):
content_multi = load(multi_path)
else:
content_multi = self._dep_props
content_multi = Template(content_multi).render({"name": name})

# parse the multi_file and add new import statement if needed
dom = minidom.parseString(content_multi)
import_deps, import_vars = dom.getElementsByTagName('ImportGroup')

# Transitive Deps
children = import_deps.getElementsByTagName("Import")
for dep in cpp_info.public_deps:
dep_props_name = "conan_%s.props" % dep
dep_imported = "'$(conan_%s_props_imported)' != 'True'" % dep
for node in children:
if (dep_props_name == node.getAttribute("Project") and
dep_imported == node.getAttribute("Condition")):
break # the import statement already exists
else: # create a new import statement
import_node = dom.createElement('Import')
import_node.setAttribute('Condition', dep_imported)
import_node.setAttribute('Project', dep_props_name)
import_deps.appendChild(import_node)
import_vars = dom.getElementsByTagName('ImportGroup')[0]

# Current vars
children = import_vars.getElementsByTagName("Import")
for node in children:
if (vars_props_name == node.getAttribute("Project") and
if (dep_props_filename == node.getAttribute("Project") and
condition == node.getAttribute("Condition")):
break # the import statement already exists
else: # create a new import statement
import_node = dom.createElement('Import')
import_node.setAttribute('Condition', condition)
import_node.setAttribute('Project', vars_props_name)
import_node.setAttribute('Project', dep_props_filename)
import_vars.appendChild(import_node)

content_multi = dom.toprettyxml()
content_multi = "\n".join(line for line in content_multi.splitlines() if line.strip())
return content_multi

def _all_props_file(self, name_general, deps):
""" this is a .props file including all declared dependencies
"""
multi_path = os.path.join(self.output_path, name_general)
if os.path.isfile(multi_path):
content_multi = load(multi_path)
else:
content_multi = self._all_props

# parse the multi_file and add a new import statement if needed
dom = minidom.parseString(content_multi)
import_group = dom.getElementsByTagName('ImportGroup')[0]
children = import_group.getElementsByTagName("Import")
for dep in deps:
conf_props_name = "conan_%s.props" % dep.name
for node in children:
if conf_props_name == node.getAttribute("Project"):
# the import statement already exists
break
else:
# create a new import statement
import_node = dom.createElement('Import')
dep_imported = "'$(conan_%s_props_imported)' != 'True'" % dep.name
import_node.setAttribute('Project', conf_props_name)
import_node.setAttribute('Condition', dep_imported)
# add it to the import group
import_group.appendChild(import_node)
content_multi = dom.toprettyxml()
# To remove all extra blank lines
content_multi = "\n".join(line for line in content_multi.splitlines() if line.strip())
return content_multi

def _content(self):
# We cannot use self._conanfile.warn(), because that fails for virtual conanfile
print("*** The 'msbuild' generator is EXPERIMENTAL ***")
Expand All @@ -251,17 +260,19 @@ def _content(self):
condition = self._condition()
# Include all direct build_requires for host context. This might change
direct_deps = self._conanfile.dependencies.direct_host_requires
result[general_name] = self._deps_props(general_name, direct_deps)
result[general_name] = self._all_props_file(general_name, direct_deps)
for dep in self._conanfile.dependencies.host_requires:
cpp_info = DepCppInfo(dep.cpp_info) # To account for automatic component aggregation
public_deps = [d.name for d in dep.dependencies.direct_host_requires]
# One file per configuration, with just the variables
vars_props_name = "conan_%s%s.props" % (dep.name, conf_name)
vars_conf_content = self._pkg_config_props(dep.name, cpp_info)
result[vars_props_name] = vars_conf_content
vars_props_name = "conan_%s_vars%s.props" % (dep.name, conf_name)
result[vars_props_name] = self._vars_props_file(dep.name, cpp_info, public_deps)
props_name = "conan_%s%s.props" % (dep.name, conf_name)
result[props_name] = self._conf_props_file(dep.name, vars_props_name, public_deps)

# The entry point for each package, it will have conditionals to the others
props_name = "conan_%s.props" % dep.name
dep_content = self._pkg_props(props_name, dep.name, vars_props_name, condition, cpp_info)
result[props_name] = dep_content
dep_name = "conan_%s.props" % dep.name
dep_content = self._dep_props_file(dep.name, dep_name, props_name, condition)
result[dep_name] = dep_content

return result
13 changes: 9 additions & 4 deletions conans/test/functional/toolchains/microsoft/test_msbuilddeps.py
Expand Up @@ -455,7 +455,7 @@ def test_install_reference(self):
client.run("install mypkg/0.1@ -g MSBuildDeps")
self.assertIn("Generator 'MSBuildDeps' calling 'generate()'", client.out)
# https://github.com/conan-io/conan/issues/8163
props = client.load("conan_mypkg_release_x64.props") # default Release/x64
props = client.load("conan_mypkg_vars_release_x64.props") # default Release/x64
folder = props[props.find("<ConanmypkgRootFolder>")+len("<ConanmypkgRootFolder>")
:props.find("</ConanmypkgRootFolder>")]
self.assertTrue(os.path.isfile(os.path.join(folder, "conaninfo.txt")))
Expand Down Expand Up @@ -610,6 +610,7 @@ def build(self):
client.run("create . pkg/0.1@")
self.assertIn("Conan_tools.props in deps", client.out)


@parameterized.expand([("['*']", True, True),
("['pkga']", True, False),
("['pkgb']", False, True),
Expand All @@ -625,7 +626,9 @@ def test_exclude_code_analysis(self, pattern, exclude_a, exclude_b):
client.run("create . pkgb/1.0@")

conanfile = textwrap.dedent("""
from conans import ConanFile, MSBuild
from conans import ConanFile
from conan.tools.microsoft import MSBuild

class HelloConan(ConanFile):
settings = "os", "build_type", "compiler", "arch"
requires = "pkgb/1.0@", "pkga/1.0"
Expand All @@ -636,15 +639,17 @@ def build(self):
""")
profile = textwrap.dedent("""
include(default)
build_type=Release
arch=x86_64
[conf]
tools.microsoft.msbuilddeps:exclude_code_analysis = %s
""" % pattern)

client.save({"conanfile.py": conanfile,
"profile": profile})
client.run("install . --profile profile")
depa = client.load("conan_pkga.props")
depb = client.load("conan_pkgb.props")
depa = client.load("conan_pkga_release_x64.props")
depb = client.load("conan_pkgb_release_x64.props")

if exclude_a:
inc = "$(ConanpkgaIncludeDirectories)"
Expand Down