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)
import os
import re
import sys
from contextlib import contextmanager
from typing import Callable
from llnl.util.lang import nullcontext
import spack.build_environment
import spack.config
import spack.error
import spack.spec
import spack.util.environment as environment
import spack.util.prefix as prefix
from spack import traverse
from spack.context import Context
@ -70,22 +66,6 @@ def unconditional_environment_modifications(view):
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(
*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)
"""
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:
maybe_projected = projected_prefix(*specs, projection=view.get_projection_for_spec)
else:
maybe_projected = nullcontext()
with maybe_projected:
# Static environment changes (prefix inspections)
for s in reversed(list(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()
dynamic = setup_context.get_env_modifications()
env.extend(dynamic)
prefix_to_prefix = {
s.prefix: view.get_projection_for_spec(s)
for s in reversed(topo_ordered)
if not s.external
}
# Avoid empty regex if all external
if not prefix_to_prefix:
return env
prefix_regex = re.compile("|".join(re.escape(p) for p in prefix_to_prefix.keys()))
for mod in env.env_modifications:
if isinstance(mod, environment.NameValueModifier):
mod.value = prefix_regex.sub(lambda m: prefix_to_prefix[m.group(0)], mod.value)
return env