Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 180 additions & 47 deletions uberenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ def parse_args():
default=False,
help="Only install (using pre-setup Spack and environment).")

# Spack skip externals
parser.add_argument("--spack-skip-externals",
dest="spack_skip_externals",
default=None,
nargs="+",
help="Skip spack finding any externals")

# Spack externals list
parser.add_argument("--spack-externals",
dest="spack_externals",
Expand Down Expand Up @@ -423,14 +430,14 @@ def set_from_args_or_json(self,setting, optional=True):
setting_value = self.args[setting]
return setting_value

def set_from_json(self,setting, optional=True):
def set_from_json(self,setting, optional=True, default_value=None):
"""
When optional=False:
If the setting key is not in the json file, error and raise an exception.
When optional=True:
If the setting key is not in the json file or args, return None.
If the setting key is not in the json file or args, return {default_value}.
"""
setting_value = None
setting_value = default_value
try:
setting_value = self.project_args[setting]
except (KeyError):
Expand Down Expand Up @@ -588,13 +595,15 @@ class SpackEnv(UberEnv):

def __init__(self, args, extra_args):
UberEnv.__init__(self,args,extra_args)

self.pkg_version = self.set_from_json("package_version")
self.pkg_src_dir = self.set_from_args_or_json("package_source_dir", True)
self.pkg_final_phase = self.set_from_args_or_json("package_final_phase", True)
self.build_mode = self.set_from_args_or_json("spack_build_mode", True)
self.spack_skip_externals = self.set_from_args_or_json("spack_skip_externals", True)
self.spack_externals = self.set_from_args_or_json("spack_externals", True)
self.spack_compiler_paths = self.set_from_args_or_json("spack_compiler_paths", True)
default_dict = {}
self.spack_host_config_patches = self.set_from_json("spack_host_config_patches", True, default_dict)

# default spack build mode is dev-build
if self.build_mode is None:
Expand Down Expand Up @@ -642,14 +651,15 @@ def __init__(self, args, extra_args):

print("[spack spec: {0}]".format(self.args["spec"]))

# Appends spec to package name (Example: '[email protected]%gcc')
self.pkg_name_with_spec = "'{0}{1}'".format(self.pkg_name, self.args["spec"])

# List of concretizer options not in all versions of spack
# (to be checked if it exists after cloning spack)
self.fresh_exists = False
self.reuse_exists = False

def pkg_name_with_spec(self,quote="'"):
# Appends spec to package name (Example: '[email protected]%gcc')
return "{0}{1}{2}{0}".format(quote,self.pkg_name, self.args["spec"])

# Spack executable (will include environment -e option by default)
def spack_exe(self, use_spack_env = True):
exe = pjoin(self.dest_dir, "spack/bin/spack")
Expand Down Expand Up @@ -799,17 +809,22 @@ def find_spack_pkg_path_from_hash(self, pkg_name, pkg_hash):
# TODO: at least print a warning when several choices exist. This will
# pick the first in the list.
if l.startswith(pkg_name) and len(l.split()) > 1:
return {"name": pkg_name, "path": l.split()[-1]}
pkg_path = l.split()[-1]
if os.path.exists(pkg_path):
return {"name": pkg_name, "path": pkg_path}
print("[ERROR: Failed to find package from hash named '{0}' with hash '{1}']".format(pkg_name, pkg_hash))
sys.exit(-1)

def find_spack_pkg_path(self, pkg_name, spec = ""):
res, out = sexe("{0} find -p {1}".format(self.spack_exe(),self.pkg_name_with_spec), ret_output = True)
requested_pkg_name_with_spec = "'{0}{1}'".format(pkg_name,spec)
res, out = sexe("{0} find -p {1}".format(self.spack_exe(), requested_pkg_name_with_spec), ret_output = True)
for l in out.split("\n"):
# TODO: at least print a warning when several choices exist. This will
# pick the first in the list.
if l.startswith(pkg_name) and len(l.split()) > 1:
return {"name": pkg_name, "path": l.split()[-1]}
pkg_path = l.split()[-1]
if os.path.exists(pkg_path):
return {"name": pkg_name, "path": pkg_path}
print("[ERROR: Failed to find package from spec named '{0}' with spec '{1}']".format(pkg_name, spec))
sys.exit(-1)

Expand Down Expand Up @@ -862,9 +877,9 @@ def clone_repo(self):
def disable_spack_config_scopes(self):
# disables all config scopes except "defaults", which we will
# force our settings into
spack_lib_config = pjoin(self.dest_spack,"lib","spack","spack","config.py")
print("[disabling config scope (except defaults) in: {0}]".format(spack_lib_config))
cfg_script = open(spack_lib_config).read()
spack_lib_config_src = pjoin(self.dest_spack,"lib","spack","spack","config.py")
print("[disabling config scope (except defaults) in: {0}]".format(spack_lib_config_src))
cfg_script = open(spack_lib_config_src).read()
#
# For newer versions of spack, we can use the SPACK_DISABLE_LOCAL_CONFIG
# env var plumbing. We patch it to True to make a permanent change.
Expand All @@ -884,17 +899,59 @@ def disable_spack_config_scopes(self):
"('user', spack.paths.user_config_path)"]:
cfg_script = cfg_script.replace(cfg_scope_stmt,
"#DISABLED BY UBERENV: " + cfg_scope_stmt)
open(spack_lib_config,"w").write(cfg_script)
# write the updated source
open(spack_lib_config_src,"w").write(cfg_script)

# disable user cache dir
spack_cache_path_dir = pjoin(self.dest_dir,"spack_cache")
spack_lib_paths_src = pjoin(self.dest_spack,"lib","spack","spack","paths.py")
path_script = open(spack_lib_paths_src).read()
print("[disabling user spack cache dir in: {0}]".format(spack_lib_paths_src))
spack_ucache_stmt = 'return os.path.expanduser(os.getenv("SPACK_USER_CACHE_PATH") or "~%s.spack" % os.sep)'
print(path_script.count(spack_ucache_stmt))
if path_script.count(spack_ucache_stmt) > 0:
path_script = path_script.replace(spack_ucache_stmt,'return "{0}"'.format(spack_cache_path_dir))
# write the updated source
open(spack_lib_paths_src,"w").write(path_script)

# 1.1.0+ logic to limit config scopes
#####
# NOTE: As of 1.1.0 this is a way to disable settings from ~/.spack
# however, without hacks above - other aspects of spack still use ~/.spack
# (one example is repo locks)
# spack team is working on an isolate command to solve this
#####
spack_settings_config_file = pjoin(self.dest_spack,"etc","spack","include.yaml")
uberenv_settings ="""
include:
# UBERENV: ONLY USE site configuration scope
- name: "site"
path: "$spack/etc/spack/site"
optional: false
"""
open(spack_settings_config_file,"w").write(uberenv_settings)

def patch(self):
# this is an opportunity to show spack python info post obtaining spack
self.print_spack_python_info()

def set_spack_bootstrap_dir(self):
# force bootstrap into dest dir
# set bootstrap location in dest dir
bstrp_cmd = "{0} bootstrap root --scope=site {1}".format(self.spack_exe(use_spack_env=False),
pjoin(self.dest_dir,"spack_bootstrap"))
res = sexe(bstrp_cmd, echo=True)


def patch(self):
# force spack to use only "defaults" config scope
self.disable_spack_config_scopes()

# set bootstrap dir to avoid conflicts with ~/.spack
self.set_spack_bootstrap_dir()

# this is an opportunity to show spack python info post obtaining spack
self.print_spack_python_info()

# setup clingo (unless specified not to)
if "spack_setup_clingo" in self.project_args and self.project_args["spack_setup_clingo"] == False:
if "spack_setup_clingo" in self.project_args and self.project_args["spack_setup_clingo"].lower() == "false":
print("[info: clingo will not be installed by uberenv]")
else:
self.setup_clingo()
Expand Down Expand Up @@ -990,25 +1047,25 @@ def create_spack_env(self):
print("[ERROR: Failed to setup Spack Environment]")
sys.exit(-1)

# Finding externals
spack_external_find_cmd = "{0} external find --not-buildable".format(self.spack_exe())
if self.spack_externals is None:
print("[finding all packages Spack knows about]")
spack_external_find_cmd = "{0} --all".format(spack_external_find_cmd)
else:
print("[finding packages from list]")
spack_external_find_cmd = "{0} {1}".format(spack_external_find_cmd, self.spack_externals)
res_external = sexe(spack_external_find_cmd, echo=True)
if res_external != 0:
print("[ERROR: Failed to setup Spack Environment]")
sys.exit(-1)
if self.spack_skip_externals is None:
# Finding externals
spack_external_find_cmd = "{0} external find --not-buildable".format(self.spack_exe())
if self.spack_externals is None:
print("[finding all packages Spack knows about]")
spack_external_find_cmd = "{0} --all".format(spack_external_find_cmd)
else:
print("[finding packages from list]")
spack_external_find_cmd = "{0} {1}".format(spack_external_find_cmd, self.spack_externals)
res_external = sexe(spack_external_find_cmd, echo=True)
if res_external != 0:
print("[ERROR: Failed to setup Spack Environment]")
sys.exit(-1)

# Copy spack.yaml to where you called package source dir
generated_spack_yaml = pjoin(self.spack_env_directory, "spack.yaml")
copied_spack_yaml = pjoin(pabs(self.pkg_src_dir), "spack.yaml")
print("[copying spack yaml file to {0}]".format(copied_spack_yaml))
sexe("cp {0} {1}".format(generated_spack_yaml, copied_spack_yaml))

print("[setup environment]")

# For each package path (if there is a repo.yaml), add Spack repository to environment
Expand All @@ -1024,22 +1081,43 @@ def create_spack_env(self):
print("[ERROR: No Spack repo.yaml detected in {0}]".format(spack_pkg_repo))
sys.exit(-1)

# Add spack package
print("[adding spack package]")
# Add main spack package
print("[adding spack package: {0}]".format(self.pkg_name_with_spec()))
spack_add_cmd = "{0} add {1}".format(self.spack_exe(),
self.pkg_name_with_spec)
self.pkg_name_with_spec())
res = sexe(spack_add_cmd, echo=True)
if res != 0:
print(f"[ERROR: Failed to add Spack spec '{self.pkg_name_with_spec}']")
print(f"[ERROR: Failed to add Spack spec '{0}']".format(self.pkg_name_with_spec()))
sys.exit(-1)

# For dev-build, call develop
if self.build_mode == "dev-build":
print("[calling spack develop]")
spack_develop_cmd = "{0} develop --no-clone --path={1} {2}@={3}".format(
self.spack_exe(), self.pkg_src_dir, self.pkg_name, self.pkg_version)
self.spack_exe(), self.pkg_src_dir, self.pkg_name_with_spec(), self.pkg_version)

sexe(spack_develop_cmd, echo=True)

def symlink_spack_env_view(self):
"""
Symlink spack view for easy access.
"""
py_script = "env = spack.environment.active_environment();"
py_script += 'print(env.views["default"].get_projection_for_spec(env.matching_spec("{0}")))'.format(self.pkg_name_with_spec(quote=""))
spack_vfind_cmd = "{0} python -c '{1}'".format(self.spack_exe(),py_script)
res, out = sexe(spack_vfind_cmd, ret_output=True, echo=True)
if res != 0:
print("[failed to find spack view info]")
sys.exit(-1)
# this will be one level down from the spack view dir, for example:
# 'spack_env/view/python-3.10.10'
view_src_path = pjoin(self.spack_env_directory, os.path.split(out)[0])
view_symlink_path = pjoin(self.dest_dir,"spack_view")
if os.path.islink(view_symlink_path):
os.unlink(view_symlink_path)
print("[symlinking env view {0} to {1} ]".format(view_src_path,view_symlink_path))
os.symlink(view_src_path,view_symlink_path)

def concretize_spack_env(self):
# Spack concretize
print("[concretizing spack env]")
Expand Down Expand Up @@ -1089,7 +1167,7 @@ def show_info(self):
# testing that the path exists is mandatory until Spack team fixes
# https://github.com/spack/spack/issues/16329
if os.path.isdir(install_path):
print("[Warning: {0} has already been installed in {1}]".format(self.pkg_name_with_spec,install_path))
print("[Warning: {0} has already been installed in {1}]".format(self.pkg_name_with_spec(),install_path))
print("[Warning: Uberenv will proceed using this directory]")
self.use_install = True

Expand All @@ -1108,7 +1186,7 @@ def install(self):
install_cmd += "-k "

# install flags
install_cmd += "install "
install_cmd += "install --fail-fast "
install_cmd = self.add_concretizer_args(install_cmd)
if self.build_mode == "dev-build":
install_cmd += "--keep-stage "
Expand All @@ -1125,18 +1203,19 @@ def install(self):
if res != 0:
print("[ERROR: Failure of spack install]")
return res

# when using install or uberenv-pkg mode, create a symlink to the host config
if self.build_mode == "install" or \
self.build_mode == "uberenv-pkg" \
or self.use_install:
self.build_mode == "uberenv-pkg" or \
self.use_install:
self.symlink_spack_env_view()
# only create a symlink if you're completing all phases
if self.pkg_final_phase == None or self.pkg_final_phase == "install":
# use spec_hash to locate b/c other helper won't work if complex
# deps are provided in the spec (e.g: @ver+variant ^package+variant)
pkg_path = self.find_spack_pkg_path_from_hash(self.pkg_name, self.spec_hash)
if self.pkg_name != pkg_path["name"]:
if self.pkg_name != pkg_path["name"] or not os.path.exists(pkg_path["path"]):
print("[ERROR: Could not find install of {0} with hash {1}]".format(self.pkg_name,self.spec_hash))
print("[package info: {0}]".format(str(pkg_path)))
return -1
else:
# Symlink host-config file
Expand All @@ -1151,6 +1230,12 @@ def install(self):
sexe("rm -f {0}".format(hc_symlink_path))
print("[symlinking host config file {0} to {1}]".format(hc_path,hc_symlink_path))
os.symlink(hc_path,hc_symlink_path)
# NOTE: you may want this for dev build as well
if len(self.spack_host_config_patches) > 0:
hc_patch_path = os.path.splitext(hc_fname)[0] + "-patch.cmake"
hc_patch_path = pjoin(self.dest_dir,hc_patch_path)
if self.patch_host_config_for_spack_view(hc_symlink_path,hc_patch_path) == -1:
return -1
# if user opt'd for an install, we want to symlink the final
# install to an easy place:
# Symlink install directory
Expand Down Expand Up @@ -1184,6 +1269,44 @@ def install(self):
print("[ERROR: Unsupported build mode: {0}]".format(self.build_mode))
return -1

def patch_host_config_for_spack_view(self, host_config_file_src, host_config_file_patched):
"""
Patch host config entries to point final spack python view paths
"""
#
# NOTE:
#
# This isn't pretty, this logic exists to solve a chicken vs egg issue
# with how spack views are setup.
#
# We need to use the final python view layout created by spack if we
# want our dependent python modules to work outside of spack without
# env var gymnastics.
#
# This method replaces cmake cache entries with new entires that point
# to the result of a spack view path glob.
#
print("[patching spack view paths into host config file {0} to create {1}]".format(host_config_file_src,host_config_file_patched))
hc_lines = open(host_config_file_src).readlines()
hc_out = open(host_config_file_patched,"w")
for l in hc_lines:
found = False
for k,v in self.spack_host_config_patches.items():
if l.count(k) > 0:
found = True
# find view path
view_search_path = os.path.join(self.dest_dir,v)
view_path_glob = glob.glob(view_search_path)
if len(view_path_glob) == 0:
print("[ERROR: Could not find view entry {0} path: {1}]".format(k,view_search_path))
return -1
# replace existing entry with what we found
entry = " # NOTE: Pathed by uberenv to use spack view path instead of spack build path\n"
entry += 'set({0} "{1}" CACHE PATH "")\n'.format(k,view_path_glob[0])
hc_out.write(entry)
if not found:
hc_out.write(l)

def get_mirror_path(self):
mirror_path = self.args["mirror"]
if not mirror_path:
Expand All @@ -1202,7 +1325,7 @@ def create_mirror(self):
if self.args["ignore_ssl_errors"]:
mirror_cmd += "-k "
mirror_cmd += "mirror create -d {0} --dependencies {1}".format(
mirror_path, self.pkg_name_with_spec)
mirror_path, self.pkg_name_with_spec())
return sexe(mirror_cmd, echo=True)

def find_spack_mirror(self, mirror_name):
Expand Down Expand Up @@ -1300,10 +1423,20 @@ def setup_clingo(self):
print("[ERROR: 'spack bootstrap now' failed with returncode {0}]".format(res))
sys.exit(-1)

res = sexe('{0} bootstrap status'.format(self.spack_exe(use_spack_env = False)), echo=True)
if res != 0:
print("[ERROR: 'spack bootstrap status' failed with returncode {0}]".format(res))
sys.exit(-1)
#####
# NOTE: Spack 1.1.0 has an issue with bootstrap now related to gpg2
#####
# `spack bootstrap status` fails post successful `spac bootstrap now`
#
# [PASS] Core Functionalities
# [FAIL] Binary packages
# [B] MISSING "gpg2": required to sign/verify buildcaches
#. Spack will take care of bootstrapping any missing dependency marked as [B]. Dependencies marked as [-] are instead required to be found on the system.
#
# res = sexe('{0} bootstrap status'.format(self.spack_exe(use_spack_env = False)), echo=True)
# if res != 0:
# print("[ERROR: 'spack bootstrap status' failed with returncode {0}]".format(res))
# sys.exit(-1)


def find_osx_sdks():
Expand Down
Loading