diff options
-rw-r--r-- | lib/spack/spack/compilers/msvc.py | 181 |
1 files changed, 145 insertions, 36 deletions
diff --git a/lib/spack/spack/compilers/msvc.py b/lib/spack/spack/compilers/msvc.py index c9efa264ee..7e91e8eca2 100644 --- a/lib/spack/spack/compilers/msvc.py +++ b/lib/spack/spack/compilers/msvc.py @@ -29,6 +29,90 @@ fortran_mapping = { } +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}" + + def get_valid_fortran_pth(comp_ver): cl_ver = str(comp_ver) sort_fn = lambda fc_ver: StrictVersion(fc_ver) @@ -75,22 +159,48 @@ class Msvc(Compiler): # file based on compiler executable path. def __init__(self, *args, **kwargs): - new_pth = [pth if pth else get_valid_fortran_pth(args[0].version) for pth in args[3]] - args[3][:] = new_pth + # This positional argument "paths" is later parsed and process by the base class + # via the call to `super` later in this method + paths = args[3] + # This positional argument "cspec" is also parsed and handled by the base class + # constructor + cspec = args[0] + new_pth = [pth if pth else get_valid_fortran_pth(cspec.version) for pth in paths] + paths[:] = new_pth super().__init__(*args, **kwargs) - if os.getenv("ONEAPI_ROOT"): + # 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(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("-", "_") + self.vcvars_call = VCVarsInvocation(vcvars_script_path, arch, self.msvc_version) + env_cmds.append(self.vcvars_call) + # Below is a check for a valid fortran path + # paths has c, cxx, fc, and f77 paths in that order + # paths[2] refers to the fc path and is a generic check + # for a fortran compiler + if paths[2]: # If this found, it sets all the vars - self.setvarsfile = os.path.join(os.getenv("ONEAPI_ROOT"), "setvars.bat") - else: - # 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. - self.setvarsfile = os.path.abspath(os.path.join(self.cc, "../../../../../../..")) - self.setvarsfile = os.path.join(self.setvarsfile, "Auxiliary", "Build", "vcvars64.bat") + oneapi_root = os.getenv("ONEAPI_ROOT") + oneapi_root_setvars = os.path.join(oneapi_root, "setvars.bat") + oneapi_version_setvars = os.path.join( + oneapi_root, "compiler", str(self.ifx_version), "env", "vars.bat" + ) + # order matters here, the specific version env must be invoked first, + # otherwise it will be ignored if the root setvars sets up the oneapi + # env first + env_cmds.extend( + [VarsInvocation(oneapi_version_setvars), VarsInvocation(oneapi_root_setvars)] + ) + self.msvc_compiler_environment = CmdCall(*env_cmds) @property def msvc_version(self): @@ -119,17 +229,31 @@ class Msvc(Compiler): """ return self.msvc_version[:2].joined.string[:3] - @property - def cl_version(self): - """Cl toolset version""" + def _compiler_version(self, compiler): + """Returns version object for given compiler""" + # ignore_errors below is true here due to ifx's + # non zero return code if it is not provided + # and input file return Version( re.search( Msvc.version_regex, - spack.compiler.get_compiler_version_output(self.cc, version_arg=None), + spack.compiler.get_compiler_version_output( + compiler, version_arg=None, ignore_errors=True + ), ).group(1) ) @property + def cl_version(self): + """Cl toolset version""" + return self._compiler_version(self.cc) + + @property + def ifx_version(self): + """Ifx compiler version associated with this version of MSVC""" + return self._compiler_version(self.fc) + + @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 @@ -146,27 +270,12 @@ class Msvc(Compiler): # output, sort into dictionary, use that to make the build # environment. - # get current platform architecture and format for vcvars argument - arch = spack.platforms.real_host().default.lower() - arch = arch.replace("-", "_") # vcvars can target specific sdk versions, force it to pick up concretized sdk # version, if needed by spec - sdk_ver = ( - "" - if "win-sdk" not in pkg.spec or pkg.name == "win-sdk" - else pkg.spec["win-sdk"].version.string + ".0" - ) - # provide vcvars with msvc version selected by concretization, - # not whatever it happens to pick up on the system (highest available version) - out = subprocess.check_output( # novermin - 'cmd /u /c "{}" {} {} {} && set'.format( - self.setvarsfile, arch, sdk_ver, "-vcvars_ver=%s" % self.msvc_version - ), - stderr=subprocess.STDOUT, - ) - if sys.version_info[0] >= 3: - out = out.decode("utf-16le", errors="replace") # novermin + if pkg.name != "win-sdk" and "win-sdk" in pkg.spec: + self.vcvars_call.sdk_ver = pkg.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()) |