gcc-runtime: add separate package for gcc runtime libs

The gcc-runtime package adds a separate node for gcc's dynamic runtime
libraries.

This should help with:

1. binary caches where rpaths for compiler support libs cannot be
   relocated because the compiler is missing on the target system
2. creating "minimal" container images

The package is versioned like `gcc` (in principle it could be
unversioned, but Spack doesn't always guarantee not mixing compilers)
This commit is contained in:
Harmen Stoppels 2023-11-16 10:04:58 +01:00 committed by Todd Gamblin
parent 0a5f2fc94d
commit 8371bb4e19
6 changed files with 250 additions and 1 deletions

View file

@ -36,6 +36,7 @@
import spack.config import spack.config
import spack.environment as ev import spack.environment as ev
import spack.modules import spack.modules
import spack.package_base
import spack.paths import spack.paths
import spack.platforms import spack.platforms
import spack.repo import spack.repo
@ -607,6 +608,7 @@ def setup_main_options(args):
[(key, [spack.paths.mock_packages_path])] [(key, [spack.paths.mock_packages_path])]
) )
spack.repo.PATH = spack.repo.create(spack.config.CONFIG) spack.repo.PATH = spack.repo.create(spack.config.CONFIG)
spack.package_base.WITH_GCC_RUNTIME = False
# If the user asked for it, don't check ssl certs. # If the user asked for it, don't check ssl certs.
if args.insecure: if args.insecure:

View file

@ -53,6 +53,7 @@
import spack.util.environment import spack.util.environment
import spack.util.path import spack.util.path
import spack.util.web import spack.util.web
from spack.directives import _depends_on
from spack.filesystem_view import YamlFilesystemView from spack.filesystem_view import YamlFilesystemView
from spack.install_test import ( from spack.install_test import (
PackageTest, PackageTest,
@ -76,6 +77,7 @@
"""Allowed URL schemes for spack packages.""" """Allowed URL schemes for spack packages."""
_ALLOWED_URL_SCHEMES = ["http", "https", "ftp", "file", "git"] _ALLOWED_URL_SCHEMES = ["http", "https", "ftp", "file", "git"]
WITH_GCC_RUNTIME = True
#: Filename for the Spack build/install log. #: Filename for the Spack build/install log.
_spack_build_logfile = "spack-build-out.txt" _spack_build_logfile = "spack-build-out.txt"
@ -371,6 +373,20 @@ def _wrapper(instance, *args, **kwargs):
return _execute_under_condition return _execute_under_condition
class BinaryPackage:
"""This adds a universal dependency on gcc-runtime."""
def maybe_depend_on_gcc_runtime(self):
# Do not depend on itself, and allow tests to disable this universal dep
if self.name == "gcc-runtime" or not WITH_GCC_RUNTIME:
return
for v in ["13", "12", "11", "10", "9", "8", "7", "6", "5", "4"]:
_depends_on(self, f"gcc-runtime@{v}:", type="link", when=f"%gcc@{v} platform=linux")
_depends_on(self, f"gcc-runtime@{v}:", type="link", when=f"%gcc@{v} platform=cray")
_directives_to_be_executed = [maybe_depend_on_gcc_runtime]
class PackageViewMixin: class PackageViewMixin:
"""This collects all functionality related to adding installed Spack """This collects all functionality related to adding installed Spack
package to views. Packages can customize how they are added to views by package to views. Packages can customize how they are added to views by
@ -433,7 +449,7 @@ def remove_files_from_view(self, view, merge_map):
Pb = TypeVar("Pb", bound="PackageBase") Pb = TypeVar("Pb", bound="PackageBase")
class PackageBase(WindowsRPath, PackageViewMixin, metaclass=PackageMeta): class PackageBase(WindowsRPath, PackageViewMixin, BinaryPackage, metaclass=PackageMeta):
"""This is the superclass for all spack packages. """This is the superclass for all spack packages.
***The Package class*** ***The Package class***

View file

@ -57,6 +57,11 @@
from spack.util.pattern import Bunch from spack.util.pattern import Bunch
@pytest.fixture(scope="session", autouse=True)
def drop_gcc_runtime():
spack.package_base.WITH_GCC_RUNTIME = False
def ensure_configuration_fixture_run_before(request): def ensure_configuration_fixture_run_before(request):
"""Ensure that fixture mutating the configuration run before the one where """Ensure that fixture mutating the configuration run before the one where
the function is called. the function is called.

View file

@ -33,6 +33,8 @@ spack:
elfutils: elfutils:
variants: +bzip2 ~nls +xz variants: +bzip2 ~nls +xz
require: "%gcc" require: "%gcc"
gcc-runtime:
require: "%gcc"
hdf5: hdf5:
variants: +fortran +hl +shared variants: +fortran +hl +shared
libfabric: libfabric:

View file

@ -17,6 +17,8 @@ spack:
variants: +mpi variants: +mpi
elfutils: elfutils:
variants: +bzip2 ~nls +xz variants: +bzip2 ~nls +xz
gcc-runtime:
require: "%gcc"
hdf5: hdf5:
require: "%gcc" require: "%gcc"
variants: +fortran +hl +shared variants: +fortran +hl +shared

View file

@ -0,0 +1,222 @@
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import re
from macholib import MachO, mach_o
from llnl.util import tty
from spack.package import *
from spack.util.elf import parse_elf
class GccRuntime(Package):
"""Package for GCC compiler runtime libraries"""
homepage = "https://gcc.gnu.org"
has_code = False
maintainers("haampie")
license("GPL-3.0-or-later WITH GCC-exception-3.1")
requires("%gcc")
LIBRARIES = [
"asan",
"atomic",
"gcc_s",
"gfortran",
"gomp",
"hwasan",
"itm",
"lsan",
"quadmath",
"ssp",
"stdc++",
"tsan",
"ubsan",
]
for v in [
"13.2",
"13.1",
"12.3",
"12.2",
"12.1",
"11.4",
"11.3",
"11.2",
"11.1",
"10.5",
"10.4",
"10.3",
"10.2",
"10.1",
"9.5",
"9.4",
"9.3",
"9.2",
"9.1",
"8.5",
"8.4",
"8.3",
"8.2",
"8.1",
"7.5",
"7.4",
"7.3",
"7.2",
"7.1",
"6.5",
"6.4",
"6.3",
"6.2",
"6.1",
"5.5",
"5.4",
"5.3",
"5.2",
"5.1",
"4.9.4",
"4.9.3",
"4.9.2",
"4.9.1",
"4.9.0",
"4.8.5",
"4.8.4",
"4.8.3",
"4.8.2",
"4.8.1",
"4.8.0",
"4.7.4",
"4.7.3",
"4.7.2",
"4.7.1",
"4.7.0",
"4.6.4",
"4.6.3",
"4.6.2",
"4.6.1",
"4.6.0",
"4.5.4",
"4.5.3",
"4.5.2",
"4.5.1",
"4.5.0",
]:
version(v)
requires(f"%gcc@{v}", when=f"@{v}")
def install(self, spec, prefix):
if spec.platform in ["linux", "cray", "freebsd"]:
libraries = self._get_libraries_elf()
elif spec.platform == "darwin":
libraries = self._get_libraries_macho()
else:
raise RuntimeError("Unsupported platform")
mkdir(prefix.lib)
if not libraries:
tty.warn("Could not detect any shared GCC runtime libraries")
return
for path, name in libraries:
install(path, os.path.join(prefix.lib, name))
def _get_libraries_elf(self):
"""Get the GCC runtime libraries for ELF binaries"""
cc = Executable(self.compiler.cc)
lib_regex = re.compile(rb"\blib[a-z-_]+\.so\.\d+\b")
path_and_install_name = []
for name in self.LIBRARIES:
# Look for the dynamic library that gcc would use to link,
# that is with .so extension and without abi suffix.
path = cc(f"-print-file-name=lib{name}.so", output=str).strip()
# gcc reports an absolute path on success
if not os.path.isabs(path):
continue
# Now there are two options:
# 1. the file is an ELF file
# 2. the file is a linker script referencing the actual library
with open(path, "rb") as f:
try:
# Try to parse as an ELF file
soname = parse_elf(f, dynamic_section=True).dt_soname_str.decode("utf-8")
except Exception:
# On failure try to "parse" as ld script; the actual
# library needs to be mentioned by filename.
f.seek(0)
script_matches = lib_regex.findall(f.read())
if len(script_matches) != 1:
continue
soname = script_matches[0].decode("utf-8")
# Now locate and install the runtime library
runtime_path = cc(f"-print-file-name={soname}", output=str).strip()
if not os.path.isabs(runtime_path):
continue
path_and_install_name.append((runtime_path, soname))
return path_and_install_name
def _get_libraries_macho(self):
"""Same as _get_libraries_elf but for Mach-O binaries"""
cc = Executable(self.compiler.cc)
path_and_install_name = []
for name in self.LIBRARIES:
if name == "gcc_s":
# On darwin, libgcc_s is versioned and can't be linked as -lgcc_s,
# but needs a suffix we don't know, so we parse it from the link line.
match = re.search(
r"\s-l(gcc_s\.[0-9.]+)\s", cc("-xc", "-", "-shared-libgcc", "-###", error=str)
)
if match is None:
continue
name = match.group(1)
path = cc(f"-print-file-name=lib{name}.dylib", output=str).strip()
if not os.path.isabs(path):
continue
macho = MachO.MachO(path)
# Get the LC_ID_DYLIB load command
for load_command, _, data in macho.headers[-1].commands:
if load_command.cmd == mach_o.LC_ID_DYLIB:
# Strip off @rpath/ prefix, or even an absolute path.
dylib_name = os.path.basename(data.rstrip(b"\x00").decode())
break
else:
continue
# Locate by dylib name
runtime_path = cc(f"-print-file-name={dylib_name}", output=str).strip()
if not os.path.isabs(runtime_path):
continue
path_and_install_name.append((runtime_path, dylib_name))
return path_and_install_name
@property
def libs(self):
# Currently these libs are not linkable with -l, they all have a suffix.
return LibraryList([])
@property
def headers(self):
return HeaderList([])