Windows rpath support (#31930)

Add a post-install step which runs (only) on Windows to modify an
install prefix, adding symlinks to all dependency libraries.

Windows does not have the same concept of RPATHs as Linux, but when
resolving symbols will check the local directory for dependency
libraries; by placing a symlink to each dependency library in the
directory with the library that needs it, the package can then
use all Spack-built dependencies.

Note:

* This collects dependency libraries based on Package.rpath, which
  includes only direct link dependencies
* There is no examination of libraries to check what dependencies
  they require, so all libraries of dependencies are symlinked
  into any directory of the package which contains libraries
This commit is contained in:
John W. Parent
2022-09-13 13:28:29 -04:00
committed by GitHub
parent 251d86e5ab
commit 53a7b49619
15 changed files with 277 additions and 46 deletions

View File

@@ -84,6 +84,9 @@
#: queue invariants).
STATUS_REMOVED = "removed"
is_windows = sys.platform == "win32"
is_osx = sys.platform == "darwin"
class InstallAction(object):
#: Don't perform an install
@@ -165,7 +168,9 @@ def _do_fake_install(pkg):
if not pkg.name.startswith("lib"):
library = "lib" + library
dso_suffix = ".dylib" if sys.platform == "darwin" else ".so"
plat_shared = ".dll" if is_windows else ".so"
plat_static = ".lib" if is_windows else ".a"
dso_suffix = ".dylib" if is_osx else plat_shared
# Install fake command
fs.mkdirp(pkg.prefix.bin)
@@ -180,7 +185,7 @@ def _do_fake_install(pkg):
# Install fake shared and static libraries
fs.mkdirp(pkg.prefix.lib)
for suffix in [dso_suffix, ".a"]:
for suffix in [dso_suffix, plat_static]:
fs.touch(os.path.join(pkg.prefix.lib, library + suffix))
# Install fake man page
@@ -1214,7 +1219,10 @@ def _install_task(self, task):
spack.package_base.PackageBase._verbose = spack.build_environment.start_build_process(
pkg, build_process, install_args
)
# Currently this is how RPATH-like behavior is achieved on Windows, after install
# establish runtime linkage via Windows Runtime link object
# Note: this is a no-op on non Windows platforms
pkg.windows_establish_runtime_linkage()
# Note: PARENT of the build process adds the new package to
# the database, so that we don't need to re-read from file.
spack.store.db.add(pkg.spec, spack.store.layout, explicit=explicit)

View File

@@ -97,6 +97,9 @@
_spack_configure_argsfile = "spack-configure-args.txt"
is_windows = sys.platform == "win32"
def preferred_version(pkg):
"""
Returns a sorted list of the preferred versions of the package.
@@ -182,6 +185,30 @@ def copy(self):
return other
class WindowsRPathMeta(object):
"""Collection of functionality surrounding Windows RPATH specific features
This is essentially meaningless for all other platforms
due to their use of RPATH. All methods within this class are no-ops on
non Windows. Packages can customize and manipulate this class as
they would a genuine RPATH, i.e. adding directories that contain
runtime library dependencies"""
def add_search_paths(self, *path):
"""Add additional rpaths that are not implicitly included in the search
scheme
"""
self.win_rpath.include_additional_link_paths(*path)
def windows_establish_runtime_linkage(self):
"""Establish RPATH on Windows
Performs symlinking to incorporate rpath dependencies to Windows runtime search paths
"""
if is_windows:
self.win_rpath.establish_link()
#: Registers which are the detectable packages, by repo and package name
#: Need a pass of package repositories to be filled.
detectable_packages = collections.defaultdict(list)
@@ -221,7 +248,7 @@ def to_windows_exe(exe):
plat_exe = []
if hasattr(cls, "executables"):
for exe in cls.executables:
if sys.platform == "win32":
if is_windows:
exe = to_windows_exe(exe)
plat_exe.append(exe)
return plat_exe
@@ -513,7 +540,7 @@ def test_log_pathname(test_stage, spec):
return os.path.join(test_stage, "test-{0}-out.txt".format(TestSuite.test_pkg_id(spec)))
class PackageBase(six.with_metaclass(PackageMeta, PackageViewMixin, object)):
class PackageBase(six.with_metaclass(PackageMeta, WindowsRPathMeta, PackageViewMixin, object)):
"""This is the superclass for all spack packages.
***The Package class***
@@ -753,6 +780,8 @@ def __init__(self, spec):
# Set up timing variables
self._fetch_time = 0.0
self.win_rpath = fsys.WindowsSimulatedRPath(self)
if self.is_extension:
pkg_cls = spack.repo.path.get_pkg_class(self.extendee_spec.name)
pkg_cls(self.extendee_spec)._check_extendable()
@@ -2754,6 +2783,8 @@ def rpath(self):
deps = self.spec.dependencies(deptype="link")
rpaths.extend(d.prefix.lib for d in deps if os.path.isdir(d.prefix.lib))
rpaths.extend(d.prefix.lib64 for d in deps if os.path.isdir(d.prefix.lib64))
if is_windows:
rpaths.extend(d.prefix.bin for d in deps if os.path.isdir(d.prefix.bin))
return rpaths
@property

View File

@@ -27,7 +27,7 @@
import llnl.util.lang
import llnl.util.tty as tty
from llnl.util.filesystem import mkdirp, remove_linked_tree, working_dir
from llnl.util.filesystem import copy_tree, mkdirp, remove_linked_tree, working_dir
import spack.binary_distribution
import spack.caches
@@ -803,7 +803,7 @@ def mock_store(tmpdir_factory, mock_repo_path, mock_configuration_scopes, _store
with spack.store.use_store(str(store_path)) as store:
with spack.repo.use_repositories(mock_repo_path):
_populate(store.db)
store_path.copy(store_cache, mode=True, stat=True)
copy_tree(str(store_path), str(store_cache))
# Make the DB filesystem read-only to ensure we can't modify entries
store_path.join(".spack-db").chmod(mode=0o555, rec=1)
@@ -844,7 +844,7 @@ def mutable_database(database_mutable_config, _store_dir_and_cache):
# Restore the initial state by copying the content of the cache back into
# the store and making the database read-only
store_path.remove(rec=1)
store_cache.copy(store_path, mode=True, stat=True)
copy_tree(str(store_cache), str(store_path))
store_path.join(".spack-db").chmod(mode=0o555, rec=1)

View File

@@ -5,6 +5,7 @@
import os
import shutil
import sys
import pytest
@@ -85,12 +86,11 @@ def test_pkg_attributes(install_mockery, mock_fetch, monkeypatch):
# assert baz_headers.basenames == ['baz.h']
assert baz_headers.directories == [spec["baz"].home.include]
if "platform=windows" in spec:
lib_suffix = ".lib"
elif "platform=darwin" in spec:
lib_suffix = ".so"
if sys.platform == "win32":
lib_suffix = ".dll"
elif sys.platform == "darwin":
lib_suffix = ".dylib"
else:
lib_suffix = ".so"
foo_libs = spec[foo].libs
assert foo_libs.basenames == ["libFoo" + lib_suffix]

View File

@@ -5,6 +5,7 @@
import fnmatch
import os.path
import sys
import pytest
import six
@@ -19,18 +20,30 @@
import spack.paths
is_windows = sys.platform == "win32"
@pytest.fixture()
def library_list():
"""Returns an instance of LibraryList."""
# Test all valid extensions: ['.a', '.dylib', '.so']
libs = [
"/dir1/liblapack.a",
"/dir2/libpython3.6.dylib", # name may contain periods
"/dir1/libblas.a",
"/dir3/libz.so",
"libmpi.so.20.10.1", # shared object libraries may be versioned
]
libs = (
[
"/dir1/liblapack.a",
"/dir2/libpython3.6.dylib", # name may contain periods
"/dir1/libblas.a",
"/dir3/libz.so",
"libmpi.so.20.10.1", # shared object libraries may be versioned
]
if not is_windows
else [
"/dir1/liblapack.lib",
"/dir2/libpython3.6.dll",
"/dir1/libblas.lib",
"/dir3/libz.dll",
"libmpi.dll.20.10.1",
]
)
return LibraryList(libs)
@@ -52,6 +65,16 @@ def header_list():
return h
# TODO: Remove below when llnl.util.filesystem.find_libraries becomes spec aware
plat_static_ext = "lib" if is_windows else "a"
plat_shared_ext = "dll" if is_windows else "so"
plat_apple_shared_ext = "dll" if is_windows else "dylib"
class TestLibraryList(object):
def test_repr(self, library_list):
x = eval(repr(library_list))
@@ -62,11 +85,11 @@ def test_joined_and_str(self, library_list):
s1 = library_list.joined()
expected = " ".join(
[
"/dir1/liblapack.a",
"/dir2/libpython3.6.dylib",
"/dir1/libblas.a",
"/dir3/libz.so",
"libmpi.so.20.10.1",
"/dir1/liblapack.%s" % plat_static_ext,
"/dir2/libpython3.6.%s" % plat_apple_shared_ext,
"/dir1/libblas.%s" % plat_static_ext,
"/dir3/libz.%s" % plat_shared_ext,
"libmpi.%s.20.10.1" % plat_shared_ext,
]
)
assert s1 == expected
@@ -77,11 +100,11 @@ def test_joined_and_str(self, library_list):
s3 = library_list.joined(";")
expected = ";".join(
[
"/dir1/liblapack.a",
"/dir2/libpython3.6.dylib",
"/dir1/libblas.a",
"/dir3/libz.so",
"libmpi.so.20.10.1",
"/dir1/liblapack.%s" % plat_static_ext,
"/dir2/libpython3.6.%s" % plat_apple_shared_ext,
"/dir1/libblas.%s" % plat_static_ext,
"/dir3/libz.%s" % plat_shared_ext,
"libmpi.%s.20.10.1" % plat_shared_ext,
]
)
assert s3 == expected
@@ -117,7 +140,7 @@ def test_paths_manipulation(self, library_list):
def test_get_item(self, library_list):
a = library_list[0]
assert a == "/dir1/liblapack.a"
assert a == "/dir1/liblapack.%s" % plat_static_ext
b = library_list[:]
assert type(b) == type(library_list)
@@ -126,9 +149,9 @@ def test_get_item(self, library_list):
def test_add(self, library_list):
pylist = [
"/dir1/liblapack.a", # removed from the final list
"/dir2/libmpi.so",
"/dir4/libnew.a",
"/dir1/liblapack.%s" % plat_static_ext, # removed from the final list
"/dir2/libmpi.%s" % plat_shared_ext,
"/dir4/libnew.%s" % plat_static_ext,
]
another = LibraryList(pylist)
both = library_list + another