Skip to content

Commit

Permalink
[NMakeToolchain] Refactoring to expose similar interface than other t…
Browse files Browse the repository at this point in the history
…oolchains & honor build config (#12665)

* honor conf compiler & linker flags in NMakeToolchain

* extend NMakeToolchain test

* refactoring of NMakeToolchain

* inject build type link flags

* workaround for potentially circular imports?

* import from visual

* formatting

* simplify

* typo

* define _LINK_ env var instead of LINK

* use /nologo instead of /NOLOGO

* add more tests

* fix preprocessors injection in CL when value is a string

* add comments

* handle preprocessor with empty value

* restore msvc versions in tests

* rewording

* expose less attributes

* typo

* properly handle empty definitions

* don't test again stuff already checked by check_exe_run

* restore msvc versions in tests

* remove CFLAGS, CPPFLAGS, CXXFLAGS
  • Loading branch information
SpaceIm committed Jan 30, 2023
1 parent 8683917 commit 2e43345
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 42 deletions.
121 changes: 90 additions & 31 deletions conan/tools/microsoft/nmaketoolchain.py
@@ -1,5 +1,6 @@
from conan.tools._compilers import cppstd_flag, build_type_flags
from conan.tools._compilers import cppstd_flag, build_type_flags, build_type_link_flags
from conan.tools.env import Environment
from conan.tools.microsoft.visual import msvc_runtime_flag, VCVars


class NMakeToolchain(object):
Expand All @@ -15,42 +16,100 @@ def __init__(self, conanfile):
:param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``.
"""
self._conanfile = conanfile
self._environment = None

# Flags
self.extra_cflags = []
self.extra_cxxflags = []
self.extra_ldflags = []
self.extra_defines = []

def _format_options(self, options):
return [f"{opt[0].replace('-', '/')}{opt[1:]}" for opt in options if len(opt) > 1]

def _format_defines(self, defines):
formated_defines = []
for define in defines:
if "=" in define:
# CL env-var can't accept '=' sign in /D option, it can be replaced by '#' sign:
# https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables
macro, value = define.split("=", 1)
if value and not value.isnumeric():
value = f'\\"{value}\\"'
define = f"{macro}#{value}"
formated_defines.append(f"/D{define}")
return formated_defines

@property
def cl_flags(self):
cppflags = []
build_type = self._conanfile.settings.get_safe("build_type")
if build_type in ['Release', 'RelWithDebInfo', 'MinSizeRel']:
cppflags.append("/DNDEBUG")
def _cl(self):
bt_flags = build_type_flags(self._conanfile.settings)
bt_flags = bt_flags if bt_flags else []

bt = build_type_flags(self._conanfile.settings)
if bt:
cppflags.extend(bt)
rt_flags = msvc_runtime_flag(self._conanfile)
rt_flags = [f"/{rt_flags}"] if rt_flags else []

cflags = []
cflags.extend(self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list))
cflags.extend(self.extra_cflags)

cxxflags = []
cppstd = cppstd_flag(self._conanfile.settings)
if cppstd:
cppflags.append(cppstd)
from conan.tools.microsoft import msvc_runtime_flag
flag = msvc_runtime_flag(self._conanfile)
if flag:
cppflags.append("-{}".format(flag))
return " ".join(cppflags).replace("-", "/")
cxxflags.append(cppstd)
cxxflags.extend(self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list))
cxxflags.extend(self.extra_cxxflags)

defines = []
build_type = self._conanfile.settings.get_safe("build_type")
if build_type in ["Release", "RelWithDebInfo", "MinSizeRel"]:
defines.append("NDEBUG")
defines.extend(self._conanfile.conf.get("tools.build:defines", default=[], check_type=list))
defines.extend(self.extra_defines)

return ["/nologo"] + \
self._format_options(bt_flags + rt_flags + cflags + cxxflags) + \
self._format_defines(defines)

@property
def _link(self):
bt_ldflags = build_type_link_flags(self._conanfile.settings)
bt_ldflags = bt_ldflags if bt_ldflags else []

ldflags = []
ldflags.extend(bt_ldflags)
ldflags.extend(self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list))
ldflags.extend(self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list))
ldflags.extend(self.extra_ldflags)

return ["/nologo"] + self._format_options(ldflags)

def environment(self):
# TODO: Seems we want to make this uniform, equal to other generators
if self._environment is None:
env = Environment()
# The whole injection of toolchain happens in CL env-var, the others LIBS, _LINK_
env.append("CL", self.cl_flags)
self._environment = env
return self._environment

def vars(self, scope="build"):
return self.environment.vars(self._conanfile, scope=scope)

def generate(self, scope="build"):
self.vars(scope).save_script("conannmaketoolchain")
from conan.tools.microsoft import VCVars
VCVars(self._conanfile).generate()
env = Environment()
# Injection of compile flags in CL env-var:
# https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables
env.append("CL", self._cl)
# Injection of link flags in _LINK_ env-var:
# https://learn.microsoft.com/en-us/cpp/build/reference/linking
env.append("_LINK_", self._link)
# Also define some special env-vars which can override special NMake macros:
# https://learn.microsoft.com/en-us/cpp/build/reference/special-nmake-macros
conf_compilers = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict)
if conf_compilers:
compilers_mapping = {
"AS": "asm",
"CC": "c",
"CPP": "cpp",
"CXX": "cpp",
"RC": "rc",
}
for env_var, comp in compilers_mapping.items():
if comp in conf_compilers:
env.define(env_var, conf_compilers[comp])
return env

def vars(self):
return self.environment().vars(self._conanfile, scope="build")

def generate(self, env=None, scope="build"):
env = env or self.environment()
env.vars(self._conanfile, scope=scope).save_script("conannmaketoolchain")
VCVars(self._conanfile).generate(scope=scope)
50 changes: 39 additions & 11 deletions conans/test/functional/toolchains/test_nmake_toolchain.py
Expand Up @@ -8,11 +8,19 @@
from conans.test.utils.tools import TestClient


@pytest.mark.parametrize("compiler, version, runtime, cppstd, build_type",
[("msvc", "190", "dynamic", "14", "Release"),
("msvc", "191", "static", "17", "Debug")])
@pytest.mark.parametrize(
"compiler, version, runtime, cppstd, build_type, defines, cflags, cxxflags, sharedlinkflags, exelinkflags",
[
("msvc", "190", "dynamic", "14", "Release", [], [], [], [], []),
("msvc", "190", "dynamic", "14", "Release",
["TEST_DEFINITION1", "TEST_DEFINITION2=0", "TEST_DEFINITION3=", "TEST_DEFINITION4=TestPpdValue4"],
["/GL"], ["/GL"], ["/LTCG"], ["/LTCG"]),
("msvc", "191", "static", "17", "Debug", [], [], [], [], []),
],
)
@pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows")
def test_toolchain_nmake(compiler, version, runtime, cppstd, build_type):
def test_toolchain_nmake(compiler, version, runtime, cppstd, build_type,
defines, cflags, cxxflags, sharedlinkflags, exelinkflags):
client = TestClient(path_with_spaces=False)
settings = {"compiler": compiler,
"compiler.version": version,
Expand All @@ -21,11 +29,31 @@ def test_toolchain_nmake(compiler, version, runtime, cppstd, build_type):
"build_type": build_type,
"arch": "x86_64"}

serialize_array = lambda arr: "[{}]".format(",".join([f"'{v}'" for v in arr]))
conf = {
"tools.build:defines": serialize_array(defines) if defines else "",
"tools.build:cflags": serialize_array(cflags) if cflags else "",
"tools.build:cxxflags": serialize_array(cxxflags) if cxxflags else "",
"tools.build:sharedlinkflags": serialize_array(sharedlinkflags) if sharedlinkflags else "",
"tools.build:exelinkflags": serialize_array(exelinkflags) if exelinkflags else "",
}

# Build the profile according to the settings provided
settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v)
conf = " ".join(f'-c {k}="{v}"' for k, v in conf.items() if v)
client.run("new dep/1.0 -m=cmake_lib")
client.run(f'create . -tf=None {settings} '
f'-c tools.cmake.cmaketoolchain:generator="Visual Studio 15"')
client.run(f'create . -tf=None {settings} {conf}')

# Rearrange defines to macro / value dict
conf_preprocessors = {}
for define in defines:
if "=" in define:
key, value = define.split("=", 1)
# gen_function_cpp doesn't properly handle empty macros
if value:
conf_preprocessors[key] = value
else:
conf_preprocessors[define] = "1"

conanfile = textwrap.dedent("""
from conan import ConanFile
Expand All @@ -41,17 +69,17 @@ def build(self):
all: simple.exe
.cpp.obj:
cl $(cppflags) $*.cpp
$(CPP) $*.cpp
simple.exe: simple.obj
cl $(cppflags) simple.obj
$(CPP) simple.obj
""")
client.save({"conanfile.py": conanfile,
"makefile": makefile,
"simple.cpp": gen_function_cpp(name="main", includes=["dep"], calls=["dep"])},
"simple.cpp": gen_function_cpp(name="main", includes=["dep"], calls=["dep"], preprocessor=conf_preprocessors.keys())},
clean_first=True)
client.run("install . {}".format(settings))
client.run(f"install . {settings} {conf}")
client.run("build .")
client.run_command("simple.exe")
assert "dep/1.0" in client.out
check_exe_run(client.out, "main", "msvc", version, build_type, "x86_64", cppstd)
check_exe_run(client.out, "main", "msvc", version, build_type, "x86_64", cppstd, conf_preprocessors)

0 comments on commit 2e43345

Please sign in to comment.