From 67b04f1b8dca35c687102748b67906134a5e33f3 Mon Sep 17 00:00:00 2001 From: Massimiliano Culpo Date: Mon, 11 Nov 2024 13:17:11 +0100 Subject: [PATCH] (WIP) Install mechanism --- lib/spack/spack/build_environment.py | 197 +++------------------- lib/spack/spack/build_systems/compiler.py | 180 +++++++++++++++++++- 2 files changed, 199 insertions(+), 178 deletions(-) diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py index d38617628bd..2bb8fb8e190 100644 --- a/lib/spack/spack/build_environment.py +++ b/lib/spack/spack/build_environment.py @@ -36,7 +36,6 @@ import multiprocessing import os import re -import stat import sys import traceback import types @@ -296,62 +295,10 @@ def _add_werror_handling(keep_werror, env): env.set("SPACK_COMPILER_FLAGS_REPLACE", " ".join(["|".join(item) for item in replace_flags])) -def set_compiler_environment_variables(pkg, env): +def set_wrapper_environment_variables_for_flags(pkg, env): assert pkg.spec.concrete - compiler = pkg.compiler spec = pkg.spec - # Make sure the executables for this compiler exist - compiler.verify_executables() - - # Set compiler variables used by CMake and autotools - assert all(key in compiler.link_paths for key in ("cc", "cxx", "f77", "fc")) - - # Populate an object with the list of environment modifications - # and return it - # TODO : add additional kwargs for better diagnostics, like requestor, - # ttyout, ttyerr, etc. - link_dir = spack.paths.build_env_path - - # Set SPACK compiler variables so that our wrapper knows what to - # call. If there is no compiler configured then use a default - # wrapper which will emit an error if it is used. - if compiler.cc: - env.set("SPACK_CC", compiler.cc) - env.set("CC", os.path.join(link_dir, compiler.link_paths["cc"])) - else: - env.set("CC", os.path.join(link_dir, "cc")) - if compiler.cxx: - env.set("SPACK_CXX", compiler.cxx) - env.set("CXX", os.path.join(link_dir, compiler.link_paths["cxx"])) - else: - env.set("CC", os.path.join(link_dir, "c++")) - if compiler.f77: - env.set("SPACK_F77", compiler.f77) - env.set("F77", os.path.join(link_dir, compiler.link_paths["f77"])) - else: - env.set("F77", os.path.join(link_dir, "f77")) - if compiler.fc: - env.set("SPACK_FC", compiler.fc) - env.set("FC", os.path.join(link_dir, compiler.link_paths["fc"])) - else: - env.set("FC", os.path.join(link_dir, "fc")) - - # Set SPACK compiler rpath flags so that our wrapper knows what to use - env.set("SPACK_CC_RPATH_ARG", compiler.cc_rpath_arg) - env.set("SPACK_CXX_RPATH_ARG", compiler.cxx_rpath_arg) - env.set("SPACK_F77_RPATH_ARG", compiler.f77_rpath_arg) - env.set("SPACK_FC_RPATH_ARG", compiler.fc_rpath_arg) - env.set("SPACK_LINKER_ARG", compiler.linker_arg) - - # Check whether we want to force RPATH or RUNPATH - if spack.config.get("config:shared_linking:type") == "rpath": - env.set("SPACK_DTAGS_TO_STRIP", compiler.enable_new_dtags) - env.set("SPACK_DTAGS_TO_ADD", compiler.disable_new_dtags) - else: - env.set("SPACK_DTAGS_TO_STRIP", compiler.disable_new_dtags) - env.set("SPACK_DTAGS_TO_ADD", compiler.enable_new_dtags) - if pkg.keep_werror is not None: keep_werror = pkg.keep_werror else: @@ -359,10 +306,6 @@ def set_compiler_environment_variables(pkg, env): _add_werror_handling(keep_werror, env) - # Set the target parameters that the compiler will add - isa_arg = optimization_flags(compiler, spec.target) - env.set("SPACK_TARGET_ARGS", isa_arg) - # Trap spack-tracked compiler flags as appropriate. # env_flags are easy to accidentally override. inject_flags = {} @@ -396,74 +339,27 @@ def set_compiler_environment_variables(pkg, env): env.set(flag.upper(), " ".join(f for f in env_flags[flag])) pkg.flags_to_build_system_args(build_system_flags) - env.set("SPACK_COMPILER_SPEC", str(spec.compiler)) - env.set("SPACK_SYSTEM_DIRS", SYSTEM_DIR_CASE_ENTRY) - compiler.setup_custom_environment(pkg, env) + # FIXME (compiler as nodes): recover this one in the correct packages + # compiler.setup_custom_environment(pkg, env) return env def optimization_flags(compiler, target): - if spack.compilers.is_mixed_toolchain(compiler): - msg = ( - "microarchitecture specific optimizations are not " - "supported yet on mixed compiler toolchains [check" - f" {compiler.name}@{compiler.version} for further details]" - ) - tty.debug(msg) - return "" - # Try to check if the current compiler comes with a version number or # has an unexpected suffix. If so, treat it as a compiler with a # custom spec. - compiler_version = compiler.version - version_number, suffix = archspec.cpu.version_components(compiler.version) - if not version_number or suffix: - try: - compiler_version = compiler.real_version - except spack.util.executable.ProcessError as e: - # log this and just return compiler.version instead - tty.debug(str(e)) - + version_number, _ = archspec.cpu.version_components(compiler.version.dotted_numeric_string) try: - result = target.optimization_flags(compiler.name, compiler_version.dotted_numeric_string) + result = target.optimization_flags(compiler.name, version_number) except (ValueError, archspec.cpu.UnsupportedMicroarchitecture): result = "" return result -class FilterDefaultDynamicLinkerSearchPaths: - """Remove rpaths to directories that are default search paths of the dynamic linker.""" - - def __init__(self, dynamic_linker: Optional[str]) -> None: - # Identify directories by (inode, device) tuple, which handles symlinks too. - self.default_path_identifiers: Set[Tuple[int, int]] = set() - if not dynamic_linker: - return - for path in spack.util.libc.default_search_paths_from_dynamic_linker(dynamic_linker): - try: - s = os.stat(path) - if stat.S_ISDIR(s.st_mode): - self.default_path_identifiers.add((s.st_ino, s.st_dev)) - except OSError: - continue - - def is_dynamic_loader_default_path(self, p: str) -> bool: - try: - s = os.stat(p) - return (s.st_ino, s.st_dev) in self.default_path_identifiers - except OSError: - return False - - def __call__(self, dirs: List[str]) -> List[str]: - if not self.default_path_identifiers: - return dirs - return [p for p in dirs if not self.is_dynamic_loader_default_path(p)] - - def set_wrapper_variables(pkg, env): """Set environment variables used by the Spack compiler wrapper (which have the prefix `SPACK_`) and also add the compiler wrappers to PATH. @@ -472,39 +368,8 @@ def set_wrapper_variables(pkg, env): this function computes these options in a manner that is intended to match the DAG traversal order in `SetupContext`. TODO: this is not the case yet, we're using post order, SetupContext is using topo order.""" - # Set environment variables if specified for - # the given compiler - compiler = pkg.compiler - env.extend(spack.schema.environment.parse(compiler.environment)) - - if compiler.extra_rpaths: - extra_rpaths = ":".join(compiler.extra_rpaths) - env.set("SPACK_COMPILER_EXTRA_RPATHS", extra_rpaths) - - # Add spack build environment path with compiler wrappers first in - # the path. We add the compiler wrapper path, which includes default - # wrappers (cc, c++, f77, f90), AND a subdirectory containing - # compiler-specific symlinks. The latter ensures that builds that - # are sensitive to the *name* of the compiler see the right name when - # we're building with the wrappers. - # - # Conflicts on case-insensitive systems (like "CC" and "cc") are - # handled by putting one in the /case-insensitive - # directory. Add that to the path too. - env_paths = [] - compiler_specific = os.path.join( - spack.paths.build_env_path, os.path.dirname(pkg.compiler.link_paths["cc"]) - ) - for item in [spack.paths.build_env_path, compiler_specific]: - env_paths.append(item) - ci = os.path.join(item, "case-insensitive") - if os.path.isdir(ci): - env_paths.append(ci) - - tty.debug("Adding compiler bin/ paths: " + " ".join(env_paths)) - for item in env_paths: - env.prepend_path("PATH", item) - env.set_path(SPACK_ENV_PATH, env_paths) + # Set compiler flags injected from the spec + set_wrapper_environment_variables_for_flags(pkg, env) # Working directory for the spack command itself, for debug logs. if spack.config.get("config:debug"): @@ -570,22 +435,17 @@ def set_wrapper_variables(pkg, env): lib_path = os.path.join(pkg.prefix, libdir) rpath_dirs.insert(0, lib_path) - filter_default_dynamic_linker_search_paths = FilterDefaultDynamicLinkerSearchPaths( - pkg.compiler.default_dynamic_linker - ) + # FIXME (compiler as nodes): recover this filter + # filter_default_dynamic_linker_search_paths = FilterDefaultDynamicLinkerSearchPaths( + # pkg.compiler.default_dynamic_linker + # ) # TODO: filter_system_paths is again wrong (and probably unnecessary due to the is_system_path # branch above). link_dirs should be filtered with entries from _parse_link_paths. link_dirs = list(dedupe(filter_system_paths(link_dirs))) include_dirs = list(dedupe(filter_system_paths(include_dirs))) rpath_dirs = list(dedupe(filter_system_paths(rpath_dirs))) - rpath_dirs = filter_default_dynamic_linker_search_paths(rpath_dirs) - - # TODO: implicit_rpaths is prefiltered by is_system_path, that should be removed in favor of - # just this filter. - implicit_rpaths = filter_default_dynamic_linker_search_paths(pkg.compiler.implicit_rpaths()) - if implicit_rpaths: - env.set("SPACK_COMPILER_IMPLICIT_RPATHS", ":".join(implicit_rpaths)) + # rpath_dirs = filter_default_dynamic_linker_search_paths(rpath_dirs) # Spack managed directories include the stage, store and upstream stores. We extend this with # their real paths to make it more robust (e.g. /tmp vs /private/tmp on macOS). @@ -641,22 +501,19 @@ def set_package_py_globals(pkg, context: Context = Context.BUILD): # Put spack compiler paths in module scope. (Some packages use it # in setup_run_environment etc, so don't put it context == build) link_dir = spack.paths.build_env_path - pkg_compiler = None - try: - pkg_compiler = pkg.compiler - except spack.compilers.NoCompilerForSpecError as e: - tty.debug(f"cannot set 'spack_cc': {str(e)}") - if pkg_compiler is not None: - module.spack_cc = os.path.join(link_dir, pkg_compiler.link_paths["cc"]) - module.spack_cxx = os.path.join(link_dir, pkg_compiler.link_paths["cxx"]) - module.spack_f77 = os.path.join(link_dir, pkg_compiler.link_paths["f77"]) - module.spack_fc = os.path.join(link_dir, pkg_compiler.link_paths["fc"]) - else: - module.spack_cc = None - module.spack_cxx = None - module.spack_f77 = None - module.spack_fc = None + # FIXME (compiler as nodes): make this more general, and not tied to three languages + # Maybe add a callback? + global_names = { + "c": ("spack_cc",), + "cxx": ("spack_cxx",), + "fortran": ("spack_fc", "spack_f77"), + } + for language in ("c", "cxx", "fortran"): + spec = pkg.spec.dependencies(virtuals=[language]) + value = None if not spec else os.path.join(link_dir, spec[0].package.link_paths[language]) + for name in global_names[language]: + setattr(module, name, value) # Useful directories within the prefix are encapsulated in # a Prefix object. @@ -823,7 +680,6 @@ def setup_package(pkg, dirty, context: Context = Context.BUILD): context == Context.TEST and pkg.test_requires_compiler ) if need_compiler: - set_compiler_environment_variables(pkg, env_mods) set_wrapper_variables(pkg, env_mods) # Platform specific setup goes before package specific setup. This is for setting @@ -848,11 +704,6 @@ def setup_package(pkg, dirty, context: Context = Context.BUILD): # Load modules on an already clean environment, just before applying Spack's # own environment modifications. This ensures Spack controls CC/CXX/... variables. - if need_compiler: - tty.debug("setup_package: loading compiler modules") - for mod in pkg.compiler.modules: - load_module(mod) - load_external_modules(pkg) # Make sure nothing's strange about the Spack environment. diff --git a/lib/spack/spack/build_systems/compiler.py b/lib/spack/spack/build_systems/compiler.py index 0e769c7dde0..d4f9d06b1af 100644 --- a/lib/spack/spack/build_systems/compiler.py +++ b/lib/spack/spack/build_systems/compiler.py @@ -4,15 +4,18 @@ import itertools import os import pathlib +import platform import re import sys -from typing import Dict, List, Sequence, Tuple, Union +from typing import Dict, List, Optional, Sequence, Tuple, Union + +import archspec.cpu import llnl.util.tty as tty -from llnl.util.lang import classproperty +from llnl.util.lang import classproperty, memoized -import spack.compiler import spack.package_base +import spack.paths import spack.util.executable # Local "type" for type hints @@ -43,6 +46,9 @@ class CompilerPackage(spack.package_base.PackageBase): #: Static definition of languages supported by this class compiler_languages: Sequence[str] = ["c", "cxx", "fortran"] + #: Relative path to compiler wrappers + link_paths: Dict[str, str] = {} + def __init__(self, spec: "spack.spec.Spec"): super().__init__(spec) msg = f"Supported languages for {spec} are not a subset of possible supported languages" @@ -77,14 +83,14 @@ def executables(cls) -> Sequence[str]: ] @classmethod - def determine_version(cls, exe: Path): + def determine_version(cls, exe: Path) -> str: version_argument = cls.compiler_version_argument if isinstance(version_argument, str): version_argument = (version_argument,) for va in version_argument: try: - output = spack.compiler.get_compiler_version_output(exe, va) + output = compiler_output(exe, version_argument=va) match = re.search(cls.compiler_version_regex, output) if match: return ".".join(match.groups()) @@ -142,3 +148,167 @@ def determine_compiler_paths(cls, exes: Sequence[Path]) -> Dict[str, Path]: def determine_variants(cls, exes: Sequence[Path], version_str: str) -> Tuple: # path determination is separated so it can be reused in subclasses return "", {"compilers": cls.determine_compiler_paths(exes=exes)} + + #: Returns the argument needed to set the RPATH, or None if it does not exist + rpath_arg: Optional[str] = "-Wl,-rpath," + #: Flag that needs to be used to pass an argument to the linker + linker_arg: str = "-Wl," + #: Flag used to produce Position Independent Code + pic_flag: str = "-fPIC" + #: Flag used to get verbose output + verbose_flags: str = "-v" + #: Flag to activate OpenMP support + openmp_flag: str = "-fopenmp" + + def standard_flag(self, *, language: str, standard: str) -> str: + """Returns the flag used to enforce a given standard for a language""" + if language not in self.supported_languages: + # FIXME (compiler as nodes): Use UnsupportedCompilerFlag ? + raise RuntimeError(f"{self.spec} does not provide the '{language}' language") + try: + return self._standard_flag(language=language, standard=standard) + except (KeyError, RuntimeError) as e: + raise RuntimeError( + f"{self.spec} does not provide the '{language}' standard {standard}" + ) from e + + def _standard_flag(self, *, language: str, standard: str) -> str: + raise NotImplementedError("Must be implemented by derived classes") + + @property + def disable_new_dtags(self) -> str: + if platform.system() == "Darwin": + return "" + return "--disable-new-dtags" + + @property + def enable_new_dtags(self) -> str: + if platform.system() == "Darwin": + return "" + return "--enable-new-dtags" + + def setup_dependent_build_environment(self, env, dependent_spec): + # FIXME (compiler as nodes): check if this is good enough or should be made more general + + # Populate an object with the list of environment modifications and return it + link_dir = pathlib.Path(spack.paths.build_env_path) + + for language, attr_name, wrapper_var_name, spack_var_name in [ + ("c", "cc", "CC", "SPACK_CC"), + ("cxx", "cxx", "CXX", "SPACK_CXX"), + ("fortran", "fortran", "F77", "SPACK_F77"), + ("fortran", "fortran", "FC", "SPACK_FC"), + ]: + if not hasattr(self, attr_name): + continue + + compiler = getattr(self, attr_name) + env.set(spack_var_name, compiler) + + if language not in self.link_paths: + continue + + wrapper_path = link_dir / self.link_paths.get(language) + env.set(wrapper_var_name, str(wrapper_path)) + + env.set("SPACK_CC_RPATH_ARG", self.rpath_arg) + env.set("SPACK_CXX_RPATH_ARG", self.rpath_arg) + env.set("SPACK_F77_RPATH_ARG", self.rpath_arg) + env.set("SPACK_FC_RPATH_ARG", self.rpath_arg) + env.set("SPACK_LINKER_ARG", self.linker_arg) + + # Check whether we want to force RPATH or RUNPATH + if spack.config.CONFIG.get("config:shared_linking:type") == "rpath": + env.set("SPACK_DTAGS_TO_STRIP", self.enable_new_dtags) + env.set("SPACK_DTAGS_TO_ADD", self.disable_new_dtags) + else: + env.set("SPACK_DTAGS_TO_STRIP", self.disable_new_dtags) + env.set("SPACK_DTAGS_TO_ADD", self.enable_new_dtags) + + spec = self.spec + uarch = spec.architecture.target + version_number, _ = archspec.cpu.version_components(spec.version.dotted_numeric_string) + try: + isa_arg = uarch.optimization_flags(spec.name, version_number) + except (ValueError, archspec.cpu.UnsupportedMicroarchitecture): + isa_arg = "" + + if isa_arg: + env.set("SPACK_TARGET_ARGS", isa_arg) + + env.set("SPACK_COMPILER_SPEC", spec.format("{name}{@version}{variants}{/hash:7}")) + + if spec.extra_attributes: + environment = spec.extra_attributes.get("environment") + if environment: + env.extend(spack.schema.environment.parse(environment)) + + extra_rpaths = spec.extra_attributes.get("extra_rpaths") + if extra_rpaths: + extra_rpaths = ":".join(compiler.extra_rpaths) + env.set("SPACK_COMPILER_EXTRA_RPATHS", extra_rpaths) + + # Add spack build environment path with compiler wrappers first in + # the path. We add the compiler wrapper path, which includes default + # wrappers (cc, c++, f77, f90), AND a subdirectory containing + # compiler-specific symlinks. The latter ensures that builds that + # are sensitive to the *name* of the compiler see the right name when + # we're building with the wrappers. + # + # Conflicts on case-insensitive systems (like "CC" and "cc") are + # handled by putting one in the /case-insensitive + # directory. Add that to the path too. + env_paths = [] + compiler_specific = os.path.join( + spack.paths.build_env_path, os.path.dirname(self.link_paths["c"]) + ) + for item in [spack.paths.build_env_path, compiler_specific]: + env_paths.append(item) + ci = os.path.join(item, "case-insensitive") + if os.path.isdir(ci): + env_paths.append(ci) + + tty.debug("Adding compiler bin/ paths: " + " ".join(env_paths)) + for item in env_paths: + env.prepend_path("PATH", item) + env.set_path("SPACK_ENV_PATH", env_paths) + + +@memoized +def _compiler_output( + compiler_path: Path, *, version_argument: str, ignore_errors: Tuple[int, ...] = () +) -> str: + """Returns the output from the compiler invoked with the given version argument. + + Args: + compiler_path: path of the compiler to be invoked + version_argument: the argument used to extract version information + """ + compiler = spack.util.executable.Executable(compiler_path) + compiler_invocation_args = { + "output": str, + "error": str, + "ignore_errors": ignore_errors, + "timeout": 120, + "fail_on_error": True, + } + if version_argument: + output = compiler(version_argument, **compiler_invocation_args) + else: + output = compiler(**compiler_invocation_args) + return output + + +def compiler_output( + compiler_path: Path, *, version_argument: str, ignore_errors: Tuple[int, ...] = () +) -> str: + """Wrapper for _get_compiler_version_output().""" + # This ensures that we memoize compiler output by *absolute path*, + # not just executable name. If we don't do this, and the path changes + # (e.g., during testing), we can get incorrect results. + if not os.path.isabs(compiler_path): + compiler_path = spack.util.executable.which_string(compiler_path, required=True) + + return _compiler_output( + compiler_path, version_argument=version_argument, ignore_errors=ignore_errors + )