environment_modifications_for_specs: do not mutate spec.prefix (#41737)

Sometimes env variables computed in `setup_run_environment` depend on tests
w.r.t. files in `spec.prefix`, but Spack temporarily projects `spec.prefix` to
the view. 

This is problematic for two reasons:

1. Some packages iterate over `<prefix>/bin`: they expect only the current
   package's executables, but find all linked in the view, leading to false
   positives.
2. Some packages test for `os.path.islink(...)`, which is always true in a view

`gcc` is an example that does both.

This PR lets Spack compute the environment modifications using the original
prefix, and projects to the view afterwards
This commit is contained in:
Harmen Stoppels 2023-12-19 23:33:16 +01:00 committed by GitHub
parent 494d3f9002
commit ec2729706b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -3,18 +3,14 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os import os
import re
import sys import sys
from contextlib import contextmanager
from typing import Callable
from llnl.util.lang import nullcontext
import spack.build_environment import spack.build_environment
import spack.config import spack.config
import spack.error import spack.error
import spack.spec import spack.spec
import spack.util.environment as environment import spack.util.environment as environment
import spack.util.prefix as prefix
from spack import traverse from spack import traverse
from spack.context import Context from spack.context import Context
@ -70,22 +66,6 @@ def unconditional_environment_modifications(view):
return env return env
@contextmanager
def projected_prefix(*specs: spack.spec.Spec, projection: Callable[[spack.spec.Spec], str]):
"""Temporarily replace every Spec's prefix with projection(s)"""
prefixes = dict()
for s in traverse.traverse_nodes(specs, key=lambda s: s.dag_hash()):
if s.external:
continue
prefixes[s.dag_hash()] = s.prefix
s.prefix = prefix.Prefix(projection(s))
yield
for s in traverse.traverse_nodes(specs, key=lambda s: s.dag_hash()):
s.prefix = prefixes.get(s.dag_hash(), s.prefix)
def environment_modifications_for_specs( def environment_modifications_for_specs(
*specs: spack.spec.Spec, view=None, set_package_py_globals: bool = True *specs: spack.spec.Spec, view=None, set_package_py_globals: bool = True
): ):
@ -102,26 +82,36 @@ def environment_modifications_for_specs(
been built on a different but compatible OS) been built on a different but compatible OS)
""" """
env = environment.EnvironmentModifications() env = environment.EnvironmentModifications()
topo_ordered = traverse.traverse_nodes(specs, root=True, deptype=("run", "link"), order="topo") topo_ordered = list(
traverse.traverse_nodes(specs, root=True, deptype=("run", "link"), order="topo")
)
# Static environment changes (prefix inspections)
for s in reversed(topo_ordered):
static = environment.inspect_path(
s.prefix, prefix_inspections(s.platform), exclude=environment.is_system_path
)
env.extend(static)
# Dynamic environment changes (setup_run_environment etc)
setup_context = spack.build_environment.SetupContext(*specs, context=Context.RUN)
if set_package_py_globals:
setup_context.set_all_package_py_globals()
env.extend(setup_context.get_env_modifications())
# Apply view projections if any.
if view: if view:
maybe_projected = projected_prefix(*specs, projection=view.get_projection_for_spec) prefix_to_prefix = {
else: s.prefix: view.get_projection_for_spec(s)
maybe_projected = nullcontext() for s in reversed(topo_ordered)
if not s.external
with maybe_projected: }
# Static environment changes (prefix inspections) # Avoid empty regex if all external
for s in reversed(list(topo_ordered)): if not prefix_to_prefix:
static = environment.inspect_path( return env
s.prefix, prefix_inspections(s.platform), exclude=environment.is_system_path prefix_regex = re.compile("|".join(re.escape(p) for p in prefix_to_prefix.keys()))
) for mod in env.env_modifications:
env.extend(static) if isinstance(mod, environment.NameValueModifier):
mod.value = prefix_regex.sub(lambda m: prefix_to_prefix[m.group(0)], mod.value)
# Dynamic environment changes (setup_run_environment etc)
setup_context = spack.build_environment.SetupContext(*specs, context=Context.RUN)
if set_package_py_globals:
setup_context.set_all_package_py_globals()
dynamic = setup_context.get_env_modifications()
env.extend(dynamic)
return env return env