Skip to content

Commit 53a7b49

Browse files
authored
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
1 parent 251d86e commit 53a7b49

15 files changed

Lines changed: 277 additions & 46 deletions

File tree

lib/spack/llnl/util/filesystem.py

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from llnl.util import tty
2323
from llnl.util.compat import Sequence
2424
from llnl.util.lang import dedupe, memoized
25-
from llnl.util.symlink import symlink
25+
from llnl.util.symlink import islink, symlink
2626

2727
from spack.util.executable import Executable
2828
from spack.util.path import path_to_os_path, system_path_filter
@@ -637,7 +637,11 @@ def copy_tree(src, dest, symlinks=True, ignore=None, _permissions=False):
637637
if symlinks:
638638
target = os.readlink(s)
639639
if os.path.isabs(target):
640-
new_target = re.sub(abs_src, abs_dest, target)
640+
641+
def escaped_path(path):
642+
return path.replace("\\", r"\\")
643+
644+
new_target = re.sub(escaped_path(abs_src), escaped_path(abs_dest), target)
641645
if new_target != target:
642646
tty.debug("Redirecting link {0} to {1}".format(target, new_target))
643647
target = new_target
@@ -1903,7 +1907,11 @@ def names(self):
19031907
name = x[3:]
19041908

19051909
# Valid extensions include: ['.dylib', '.so', '.a']
1906-
for ext in [".dylib", ".so", ".a"]:
1910+
# on non Windows platform
1911+
# Windows valid library extensions are:
1912+
# ['.dll', '.lib']
1913+
valid_exts = [".dll", ".lib"] if is_windows else [".dylib", ".so", ".a"]
1914+
for ext in valid_exts:
19071915
i = name.rfind(ext)
19081916
if i != -1:
19091917
names.append(name[:i])
@@ -2046,15 +2054,23 @@ def find_libraries(libraries, root, shared=True, recursive=False):
20462054
message = message.format(find_libraries.__name__, type(libraries))
20472055
raise TypeError(message)
20482056

2057+
if is_windows:
2058+
static = "lib"
2059+
shared = "dll"
2060+
else:
2061+
# Used on both Linux and macOS
2062+
static = "a"
2063+
shared = "so"
2064+
20492065
# Construct the right suffix for the library
20502066
if shared:
20512067
# Used on both Linux and macOS
2052-
suffixes = ["so"]
2068+
suffixes = [shared]
20532069
if sys.platform == "darwin":
20542070
# Only used on macOS
20552071
suffixes.append("dylib")
20562072
else:
2057-
suffixes = ["a"]
2073+
suffixes = [static]
20582074

20592075
# List of libraries we are searching with suffixes
20602076
libraries = ["{0}.{1}".format(lib, suffix) for lib in libraries for suffix in suffixes]
@@ -2067,7 +2083,11 @@ def find_libraries(libraries, root, shared=True, recursive=False):
20672083
# perform first non-recursive search in root/lib then in root/lib64 and
20682084
# finally search all of root recursively. The search stops when the first
20692085
# match is found.
2070-
for subdir in ("lib", "lib64"):
2086+
common_lib_dirs = ["lib", "lib64"]
2087+
if is_windows:
2088+
common_lib_dirs.extend(["bin", "Lib"])
2089+
2090+
for subdir in common_lib_dirs:
20712091
dirname = join_path(root, subdir)
20722092
if not os.path.isdir(dirname):
20732093
continue
@@ -2080,6 +2100,155 @@ def find_libraries(libraries, root, shared=True, recursive=False):
20802100
return LibraryList(found_libs)
20812101

20822102

2103+
def find_all_shared_libraries(root, recursive=False):
2104+
"""Convenience function that returns the list of all shared libraries found
2105+
in the directory passed as argument.
2106+
2107+
See documentation for `llnl.util.filesystem.find_libraries` for more information
2108+
"""
2109+
return find_libraries("*", root=root, shared=True, recursive=recursive)
2110+
2111+
2112+
def find_all_static_libraries(root, recursive=False):
2113+
"""Convenience function that returns the list of all static libraries found
2114+
in the directory passed as argument.
2115+
2116+
See documentation for `llnl.util.filesystem.find_libraries` for more information
2117+
"""
2118+
return find_libraries("*", root=root, shared=False, recursive=recursive)
2119+
2120+
2121+
def find_all_libraries(root, recursive=False):
2122+
"""Convenience function that returns the list of all libraries found
2123+
in the directory passed as argument.
2124+
2125+
See documentation for `llnl.util.filesystem.find_libraries` for more information
2126+
"""
2127+
2128+
return find_all_shared_libraries(root, recursive=recursive) + find_all_static_libraries(
2129+
root, recursive=recursive
2130+
)
2131+
2132+
2133+
class WindowsSimulatedRPath(object):
2134+
"""Class representing Windows filesystem rpath analog
2135+
2136+
One instance of this class is associated with a package (only on Windows)
2137+
For each lib/binary directory in an associated package, this class introduces
2138+
a symlink to any/all dependent libraries/binaries. This includes the packages
2139+
own bin/lib directories, meaning the libraries are linked to the bianry directory
2140+
and vis versa.
2141+
"""
2142+
2143+
def __init__(self, package, link_install_prefix=True):
2144+
"""
2145+
Args:
2146+
package (spack.package_base.PackageBase): Package requiring links
2147+
link_install_prefix (bool): Link against package's own install or stage root.
2148+
Packages that run their own executables during build and require rpaths to
2149+
the build directory during build time require this option. Default: install
2150+
root
2151+
"""
2152+
self.pkg = package
2153+
self._addl_rpaths = set()
2154+
self.link_install_prefix = link_install_prefix
2155+
self._internal_links = set()
2156+
2157+
@property
2158+
def link_dest(self):
2159+
"""
2160+
Set of directories where package binaries/libraries are located.
2161+
"""
2162+
if hasattr(self.pkg, "libs") and self.pkg.libs:
2163+
pkg_libs = set(self.pkg.libs.directories)
2164+
else:
2165+
pkg_libs = set((self.pkg.prefix.lib, self.pkg.prefix.lib64))
2166+
2167+
return pkg_libs | set([self.pkg.prefix.bin]) | self.internal_links
2168+
2169+
@property
2170+
def internal_links(self):
2171+
"""
2172+
linking that would need to be established within the package itself. Useful for links
2173+
against extension modules/build time executables/internal linkage
2174+
"""
2175+
return self._internal_links
2176+
2177+
def add_internal_links(self, *dest):
2178+
"""
2179+
Incorporate additional paths into the rpath (sym)linking scheme.
2180+
2181+
Paths provided to this method are linked against by a package's libraries
2182+
and libraries found at these paths are linked against a package's binaries.
2183+
(i.e. /site-packages -> /bin and /bin -> /site-packages)
2184+
2185+
Specified paths should be outside of a package's lib, lib64, and bin
2186+
directories.
2187+
"""
2188+
self._internal_links = self._internal_links | set(*dest)
2189+
2190+
@property
2191+
def link_targets(self):
2192+
"""
2193+
Set of libraries this package needs to link against during runtime
2194+
These packages will each be symlinked into the packages lib and binary dir
2195+
"""
2196+
2197+
dependent_libs = []
2198+
for path in self.pkg.rpath:
2199+
dependent_libs.extend(list(find_all_shared_libraries(path, recursive=True)))
2200+
for extra_path in self._addl_rpaths:
2201+
dependent_libs.extend(list(find_all_shared_libraries(extra_path, recursive=True)))
2202+
return set(dependent_libs)
2203+
2204+
def include_additional_link_paths(self, *paths):
2205+
"""
2206+
Add libraries found at the root of provided paths to runtime linking
2207+
2208+
These are libraries found outside of the typical scope of rpath linking
2209+
that require manual inclusion in a runtime linking scheme
2210+
2211+
Args:
2212+
*paths (str): arbitrary number of paths to be added to runtime linking
2213+
"""
2214+
self._addl_rpaths = self._addl_rpaths | set(paths)
2215+
2216+
def establish_link(self):
2217+
"""
2218+
(sym)link packages to runtime dependencies based on RPath configuration for
2219+
Windows heuristics
2220+
"""
2221+
# from build_environment.py:463
2222+
# The top-level package is always RPATHed. It hasn't been installed yet
2223+
# so the RPATHs are added unconditionally
2224+
2225+
# for each binary install dir in self.pkg (i.e. pkg.prefix.bin, pkg.prefix.lib)
2226+
# install a symlink to each dependent library
2227+
for library, lib_dir in itertools.product(self.link_targets, self.link_dest):
2228+
if not path_contains_subdirectory(library, lib_dir):
2229+
file_name = os.path.basename(library)
2230+
dest_file = os.path.join(lib_dir, file_name)
2231+
if os.path.exists(lib_dir):
2232+
try:
2233+
symlink(library, dest_file)
2234+
# For py2 compatibility, we have to catch the specific Windows error code
2235+
# associate with trying to create a file that already exists (winerror 183)
2236+
except OSError as e:
2237+
if e.winerror == 183:
2238+
# We have either already symlinked or we are encoutering a naming clash
2239+
# either way, we don't want to overwrite existing libraries
2240+
already_linked = islink(dest_file)
2241+
tty.debug(
2242+
"Linking library %s to %s failed, " % (library, dest_file)
2243+
+ "already linked."
2244+
if already_linked
2245+
else "library with name %s already exists." % file_name
2246+
)
2247+
pass
2248+
else:
2249+
raise e
2250+
2251+
20832252
@system_path_filter
20842253
@memoized
20852254
def can_access_dir(path):

lib/spack/spack/installer.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
#: queue invariants).
8585
STATUS_REMOVED = "removed"
8686

87+
is_windows = sys.platform == "win32"
88+
is_osx = sys.platform == "darwin"
89+
8790

8891
class InstallAction(object):
8992
#: Don't perform an install
@@ -165,7 +168,9 @@ def _do_fake_install(pkg):
165168
if not pkg.name.startswith("lib"):
166169
library = "lib" + library
167170

168-
dso_suffix = ".dylib" if sys.platform == "darwin" else ".so"
171+
plat_shared = ".dll" if is_windows else ".so"
172+
plat_static = ".lib" if is_windows else ".a"
173+
dso_suffix = ".dylib" if is_osx else plat_shared
169174

170175
# Install fake command
171176
fs.mkdirp(pkg.prefix.bin)
@@ -180,7 +185,7 @@ def _do_fake_install(pkg):
180185

181186
# Install fake shared and static libraries
182187
fs.mkdirp(pkg.prefix.lib)
183-
for suffix in [dso_suffix, ".a"]:
188+
for suffix in [dso_suffix, plat_static]:
184189
fs.touch(os.path.join(pkg.prefix.lib, library + suffix))
185190

186191
# Install fake man page
@@ -1214,7 +1219,10 @@ def _install_task(self, task):
12141219
spack.package_base.PackageBase._verbose = spack.build_environment.start_build_process(
12151220
pkg, build_process, install_args
12161221
)
1217-
1222+
# Currently this is how RPATH-like behavior is achieved on Windows, after install
1223+
# establish runtime linkage via Windows Runtime link object
1224+
# Note: this is a no-op on non Windows platforms
1225+
pkg.windows_establish_runtime_linkage()
12181226
# Note: PARENT of the build process adds the new package to
12191227
# the database, so that we don't need to re-read from file.
12201228
spack.store.db.add(pkg.spec, spack.store.layout, explicit=explicit)

lib/spack/spack/package_base.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@
9797
_spack_configure_argsfile = "spack-configure-args.txt"
9898

9999

100+
is_windows = sys.platform == "win32"
101+
102+
100103
def preferred_version(pkg):
101104
"""
102105
Returns a sorted list of the preferred versions of the package.
@@ -182,6 +185,30 @@ def copy(self):
182185
return other
183186

184187

188+
class WindowsRPathMeta(object):
189+
"""Collection of functionality surrounding Windows RPATH specific features
190+
191+
This is essentially meaningless for all other platforms
192+
due to their use of RPATH. All methods within this class are no-ops on
193+
non Windows. Packages can customize and manipulate this class as
194+
they would a genuine RPATH, i.e. adding directories that contain
195+
runtime library dependencies"""
196+
197+
def add_search_paths(self, *path):
198+
"""Add additional rpaths that are not implicitly included in the search
199+
scheme
200+
"""
201+
self.win_rpath.include_additional_link_paths(*path)
202+
203+
def windows_establish_runtime_linkage(self):
204+
"""Establish RPATH on Windows
205+
206+
Performs symlinking to incorporate rpath dependencies to Windows runtime search paths
207+
"""
208+
if is_windows:
209+
self.win_rpath.establish_link()
210+
211+
185212
#: Registers which are the detectable packages, by repo and package name
186213
#: Need a pass of package repositories to be filled.
187214
detectable_packages = collections.defaultdict(list)
@@ -221,7 +248,7 @@ def to_windows_exe(exe):
221248
plat_exe = []
222249
if hasattr(cls, "executables"):
223250
for exe in cls.executables:
224-
if sys.platform == "win32":
251+
if is_windows:
225252
exe = to_windows_exe(exe)
226253
plat_exe.append(exe)
227254
return plat_exe
@@ -513,7 +540,7 @@ def test_log_pathname(test_stage, spec):
513540
return os.path.join(test_stage, "test-{0}-out.txt".format(TestSuite.test_pkg_id(spec)))
514541

515542

516-
class PackageBase(six.with_metaclass(PackageMeta, PackageViewMixin, object)):
543+
class PackageBase(six.with_metaclass(PackageMeta, WindowsRPathMeta, PackageViewMixin, object)):
517544
"""This is the superclass for all spack packages.
518545
519546
***The Package class***
@@ -753,6 +780,8 @@ def __init__(self, spec):
753780
# Set up timing variables
754781
self._fetch_time = 0.0
755782

783+
self.win_rpath = fsys.WindowsSimulatedRPath(self)
784+
756785
if self.is_extension:
757786
pkg_cls = spack.repo.path.get_pkg_class(self.extendee_spec.name)
758787
pkg_cls(self.extendee_spec)._check_extendable()
@@ -2754,6 +2783,8 @@ def rpath(self):
27542783
deps = self.spec.dependencies(deptype="link")
27552784
rpaths.extend(d.prefix.lib for d in deps if os.path.isdir(d.prefix.lib))
27562785
rpaths.extend(d.prefix.lib64 for d in deps if os.path.isdir(d.prefix.lib64))
2786+
if is_windows:
2787+
rpaths.extend(d.prefix.bin for d in deps if os.path.isdir(d.prefix.bin))
27572788
return rpaths
27582789

27592790
@property

lib/spack/spack/test/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import llnl.util.lang
2929
import llnl.util.tty as tty
30-
from llnl.util.filesystem import mkdirp, remove_linked_tree, working_dir
30+
from llnl.util.filesystem import copy_tree, mkdirp, remove_linked_tree, working_dir
3131

3232
import spack.binary_distribution
3333
import spack.caches
@@ -803,7 +803,7 @@ def mock_store(tmpdir_factory, mock_repo_path, mock_configuration_scopes, _store
803803
with spack.store.use_store(str(store_path)) as store:
804804
with spack.repo.use_repositories(mock_repo_path):
805805
_populate(store.db)
806-
store_path.copy(store_cache, mode=True, stat=True)
806+
copy_tree(str(store_path), str(store_cache))
807807

808808
# Make the DB filesystem read-only to ensure we can't modify entries
809809
store_path.join(".spack-db").chmod(mode=0o555, rec=1)
@@ -844,7 +844,7 @@ def mutable_database(database_mutable_config, _store_dir_and_cache):
844844
# Restore the initial state by copying the content of the cache back into
845845
# the store and making the database read-only
846846
store_path.remove(rec=1)
847-
store_cache.copy(store_path, mode=True, stat=True)
847+
copy_tree(str(store_cache), str(store_path))
848848
store_path.join(".spack-db").chmod(mode=0o555, rec=1)
849849

850850

lib/spack/spack/test/data/directory_search/a/libc.dll

Whitespace-only changes.

lib/spack/spack/test/data/directory_search/a/libc.lib

Whitespace-only changes.

lib/spack/spack/test/data/directory_search/b/liba.dll

Whitespace-only changes.

lib/spack/spack/test/data/directory_search/b/liba.lib

Whitespace-only changes.

lib/spack/spack/test/data/directory_search/b/libd.dll

Whitespace-only changes.

lib/spack/spack/test/data/directory_search/b/libd.lib

Whitespace-only changes.

0 commit comments

Comments
 (0)