spack/var/spack/repos/builtin/packages/msvc/package.py
John W. Parent 75a3d179b1
Windows: MSVC provides fortran, fix msmpi handling (#49734)
Co-authored-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
2025-03-31 10:53:07 +02:00

321 lines
12 KiB
Python

# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os.path
import re
import subprocess
import archspec.cpu
import spack.build_systems.compiler
import spack.platforms
import spack.version
from spack.package import *
FC_PATH: Dict[str, str] = dict()
def get_latest_valid_fortran_pth():
"""Assign maximum available fortran compiler version"""
# TODO (johnwparent): validate compatibility w/ try compiler
# functionality when added
sort_fn = lambda fc_ver: Version(fc_ver)
sort_fc_ver = sorted(list(FC_PATH.keys()), key=sort_fn)
return FC_PATH[sort_fc_ver[-1]] if sort_fc_ver else None
class Msvc(Package, CompilerPackage):
"""
Microsoft Visual C++ is a compiler for the C, C++, C++/CLI and C++/CX programming languages.
"""
homepage = "https://visualstudio.microsoft.com/vs/features/cplusplus/"
def install(self, spec, prefix):
raise InstallError(
"MSVC compilers are not installable with Spack, but can be "
"detected on a system where they are externally installed"
)
compiler_languages = ["c", "cxx", "fortran"]
c_names = ["cl"]
cxx_names = ["cl"]
fortran_names = ["ifx", "ifort"]
compiler_version_argument = ""
compiler_version_regex = r"([1-9][0-9]*\.[0-9]*\.[0-9]*)"
# Due to the challenges of supporting compiler wrappers
# in Windows, we leave these blank, and dynamically compute
# based on proper versions of MSVC from there
# pending acceptance of #28117 for full support using
# compiler wrappers
compiler_wrapper_link_paths = {"c": "", "cxx": "", "fortran": ""}
provides("c", "cxx", "fortran")
requires("platform=windows", msg="MSVC is only supported on Windows")
@classmethod
def determine_version(cls, exe):
# MSVC compiler does not have a proper version argument
# Errors out and prints version info with no args
is_ifx = "ifx.exe" in str(exe)
match = re.search(
cls.compiler_version_regex,
spack.build_systems.compiler.compiler_output(
exe, version_argument=None, ignore_errors=1
),
)
if match:
if is_ifx:
FC_PATH[match.group(1)] = str(exe)
return match.group(1)
@classmethod
def determine_variants(cls, exes, version_str):
# MSVC uses same executable for both languages
spec, extras = super().determine_variants(exes, version_str)
extras["compilers"]["c"] = extras["compilers"]["cxx"]
# This depends on oneapi being processed before msvc
# which is guarunteed from detection behavior.
# Processing oneAPI tracks oneAPI installations within
# this module, which are then used to populate compatible
# MSVC version's fortran compiler spots
# TODO: remove this once #45189 lands
# TODO: interrogate intel and msvc for compatibility after
# #45189 lands
fortran_compiler = get_latest_valid_fortran_pth()
if fortran_compiler is not None:
extras["compilers"]["fortran"] = fortran_compiler
return spec, extras
def setup_dependent_build_environment(self, env, dependent_spec):
self.init_msvc()
# Set the build environment variables for spack. Just using
# subprocess.call() doesn't work since that operates in its own
# environment which is destroyed (along with the adjusted variables)
# once the process terminates. So go the long way around: examine
# output, sort into dictionary, use that to make the build
# environment.
# vcvars can target specific sdk versions, force it to pick up concretized sdk
# version, if needed by spec
if dependent_spec.name != "win-sdk" and "win-sdk" in dependent_spec:
self.vcvars_call.sdk_ver = dependent_spec["win-sdk"].version.string
out = self.msvc_compiler_environment()
int_env = dict(
(key, value)
for key, _, value in (line.partition("=") for line in out.splitlines())
if key and value
)
for env_var in int_env:
if os.pathsep not in int_env[env_var]:
env.set(env_var, int_env[env_var])
else:
env.set_path(env_var, int_env[env_var].split(os.pathsep))
env.set("CC", self.cc)
env.set("CXX", self.cxx)
if self.fortran:
env.set("FC", self.fortran)
env.set("F77", self.fortran)
def init_msvc(self):
# To use the MSVC compilers, VCVARS must be invoked
# VCVARS is located at a fixed location, referencable
# idiomatically by the following relative path from the
# compiler.
# Spack first finds the compilers via VSWHERE
# and stores their path, but their respective VCVARS
# file must be invoked before useage.
env_cmds = []
compiler_root = os.path.join(os.path.dirname(self.cc), "../../../../../..")
vcvars_script_path = os.path.join(compiler_root, "Auxiliary", "Build", "vcvars64.bat")
# get current platform architecture and format for vcvars argument
arch = spack.platforms.real_host().default.lower()
arch = arch.replace("-", "_")
if str(archspec.cpu.host().family) == "x86_64":
arch = "amd64"
msvc_version = spack.version.Version(
re.search(Msvc.compiler_version_regex, self.cc).group(1)
)
self.vcvars_call = VCVarsInvocation(vcvars_script_path, arch, msvc_version)
env_cmds.append(self.vcvars_call)
self.msvc_compiler_environment = CmdCall(*env_cmds)
def _standard_flag(self, *, language: str, standard: str) -> str:
flags = {
"cxx": {
"11": "/std:c++11",
"14": "/std:c++14",
"17": "/std:c++17",
"20": "/std:c++20",
},
"c": {"11": "/std:c11", "17": "/std:c17"},
}
return flags[language][standard]
@property
def short_msvc_version(self):
"""This is the shorthand VCToolset version of form
MSVC<short-ver>
"""
return "MSVC" + self.vc_toolset_ver
@property
def vc_toolset_ver(self):
"""
The toolset version is the version of the combined set of cl and link
This typically relates directly to VS version i.e. VS 2022 is v143
VS 19 is v142, etc.
This value is defined by the first three digits of the major + minor
version of the VS toolset (143 for 14.3x.bbbbb). Traditionally the
minor version has remained a static two digit number for a VS release
series, however, as of VS22, this is no longer true, both
14.4x.bbbbb and 14.3x.bbbbb are considered valid VS22 VC toolset
versions due to a change in toolset minor version sentiment.
This is *NOT* the full version, for that see
Msvc.msvc_version or MSVC.platform_toolset_ver for the
raw platform toolset version
"""
ver = self.msvc_version[:2].joined.string[:3]
return ver
@property
def msvc_version(self):
"""This is the VCToolset version *NOT* the actual version of the cl compiler"""
return spack.version.Version(re.search(Msvc.compiler_version_regex, self.cc).group(1))
@property
def vs_root(self):
# The MSVC install root is located at a fix level above the compiler
# and is referenceable idiomatically via the pattern below
# this should be consistent accross versions
return os.path.abspath(os.path.join(self.cc, "../../../../../../../.."))
@property
def platform_toolset_ver(self):
"""
This is the platform toolset version of current MSVC compiler
i.e. 142. The platform toolset is the targeted MSVC library/compiler
versions by compilation (this is different from the VC Toolset)
This is different from the VC toolset version as established
by `short_msvc_version`, but typically are represented by the same
three digit value
"""
# Typically VS toolset version and platform toolset versions match
# VS22 introduces the first divergence of VS toolset version
# (144 for "recent" releases) and platform toolset version (143)
# so it needs additional handling until MS releases v144
# (assuming v144 is also for VS22)
# or adds better support for detection
# TODO: (johnwparent) Update this logic for the next platform toolset
# or VC toolset version update
toolset_ver = self.vc_toolset_ver
vs22_toolset = spack.version.Version(toolset_ver) > Version("142")
return toolset_ver if not vs22_toolset else "143"
class CmdCall:
"""Compose a call to `cmd` for an ordered series of cmd commands/scripts"""
def __init__(self, *cmds):
if not cmds:
raise RuntimeError(
"""Attempting to run commands from CMD without specifying commands.
Please add commands to be run."""
)
self._cmds = cmds
def __call__(self):
out = subprocess.check_output(self.cmd_line, stderr=subprocess.STDOUT) # novermin
return out.decode("utf-16le", errors="replace") # novermin
@property
def cmd_line(self):
base_call = "cmd /u /c "
commands = " && ".join([x.command_str() for x in self._cmds])
# If multiple commands are being invoked by a single subshell
# they must be encapsulated by a double quote. Always double
# quote to be sure of proper handling
# cmd will properly resolve nested double quotes as needed
#
# `set`` writes out the active env to the subshell stdout,
# and in this context we are always trying to obtain env
# state so it should always be appended
return base_call + f'"{commands} && set"'
class VarsInvocation:
def __init__(self, script):
self._script = script
def command_str(self):
return f'"{self._script}"'
@property
def script(self):
return self._script
class VCVarsInvocation(VarsInvocation):
def __init__(self, script, arch, msvc_version):
super(VCVarsInvocation, self).__init__(script)
self._arch = arch
self._msvc_version = msvc_version
@property
def sdk_ver(self):
"""Accessor for Windows SDK version property
Note: This property may not be set by
the calling context and as such this property will
return an empty string
This property will ONLY be set if the SDK package
is a dependency somewhere in the Spack DAG of the package
for which we are constructing an MSVC compiler env.
Otherwise this property should be unset to allow the VCVARS
script to use its internal heuristics to determine appropriate
SDK version
"""
if getattr(self, "_sdk_ver", None):
return self._sdk_ver + ".0"
return ""
@sdk_ver.setter
def sdk_ver(self, val):
self._sdk_ver = val
@property
def arch(self):
return self._arch
@property
def vcvars_ver(self):
return f"-vcvars_ver={self._msvc_version}"
def command_str(self):
script = super(VCVarsInvocation, self).command_str()
return f"{script} {self.arch} {self.sdk_ver} {self.vcvars_ver}"
FC_PATH = {}
def get_valid_fortran_pth():
"""Assign maximum available fortran compiler version"""
# TODO (johnwparent): validate compatibility w/ try compiler
# functionality when added
sort_fn = lambda fc_ver: spack.version.Version(fc_ver)
sort_fc_ver = sorted(list(FC_PATH.keys()), key=sort_fn)
return FC_PATH[sort_fc_ver[-1]] if sort_fc_ver else None