Skip to content

Commit 62fa5b3

Browse files
committed
First version of spack-stack extension to check that the correct/intended compiler is used for each spec
1 parent 04c6704 commit 62fa5b3

File tree

5 files changed

+246
-46
lines changed

5 files changed

+246
-46
lines changed

spack-ext/lib/jcsda-emc/spack-stack/stack/cmd/stack.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
setup_meta_modules_parser,
99
stack_setup_meta_modules,
1010
)
11+
from spack.extensions.stack.cmd.stack_cmds.check_preferred_compiler import (
12+
setup_preferred_compiler_parser,
13+
stack_check_preferred_compiler,
14+
)
1115
from spack.extensions.stack.stack_paths import stack_path
1216

1317
description = "Create spack-stack environment"
@@ -26,10 +30,17 @@ def setup_parser(subparser):
2630
sp = subparser.add_subparsers(metavar='SUBCOMMAND', dest='stack_command')
2731
create_parser = sp.add_parser('create',
2832
help='Create spack-stack environment or container.')
29-
meta_modules_parser = sp.add_parser('setup-meta-modules',
30-
help='Create lmod/lua or tcl/tk meta-modules')
33+
meta_modules_parser = sp.add_parser(
34+
'setup-meta-modules',
35+
help='Create lmod/lua or tcl/tk meta-modules',
36+
)
37+
preferred_compiler_parser = sp.add_parser(
38+
'check-preferred-compiler',
39+
help='Check that the preferred compiler is being used',
40+
)
3141
setup_create_parser(create_parser)
3242
setup_meta_modules_parser(meta_modules_parser)
43+
setup_preferred_compiler_parser(preferred_compiler_parser)
3344

3445

3546
# Main command that calls subcommands
@@ -38,3 +49,5 @@ def stack(parser, args):
3849
stack_create(parser, args)
3950
if args.stack_command == 'setup-meta-modules':
4051
stack_setup_meta_modules(parser, args)
52+
if args.stack_command == 'check-preferred-compiler':
53+
stack_check_preferred_compiler(parser, args)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from spack.extensions.stack.compiler_utils import check_preferred_compiler
2+
3+
description = "Check preferred compiler"
4+
section = "spack-stack"
5+
level = "long"
6+
7+
8+
# Add potential arguments to check-preferred-compiler
9+
def setup_preferred_compiler_parser(subparser):
10+
pass
11+
12+
13+
def stack_check_preferred_compiler(parser, args):
14+
check_preferred_compiler()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
3+
# Terminal colors
4+
RED = "\033[91m"
5+
GREEN = "\033[92m"
6+
YELLOW = "\033[93m"
7+
BLUE = "\033[94m"
8+
RESET = "\033[0m"
9+
10+
# Colored log levels
11+
INFO_LABEL = "INFO: "
12+
ERROR_LABEL = "\033[91mERROR:\033[0m "
13+
14+
# Aliases to shorten module paths for tcl modules. These aliases must match
15+
# the compiler and MPI name translations in configs/common/modules_tcl.yaml
16+
ALIASES = {
17+
"none" : "none",
18+
# Compilers
19+
"gcc" : "gcc",
20+
"intel-oneapi-compilers-classic" : "intel",
21+
"intel-oneapi-compilers" : "oneapi",
22+
"llvm" : "llvm",
23+
# MPI
24+
"cray-mpich" : "cray-mpich",
25+
"intel-oneapi-mpi" : "impi",
26+
"mpich" : "mpich",
27+
"mpt" : "mpt",
28+
"openmpi" : "openmpi",
29+
}
30+
31+
32+
def get_preferred_compiler(config):
33+
"""Determine the preferred compiler by looking at
34+
packages:
35+
fortran:
36+
prefer:
37+
- COMPILER_NAME (gcc, intel-oneapi-compilers, llvm, ..)
38+
"""
39+
try:
40+
preferred_compilers = config.get("packages")["fortran"]["prefer"]
41+
except:
42+
raise Exception(
43+
"""Unable to detect preferred compiler from environment.
44+
Does the environment have the config entry 'packages:fortran:prefer?'"""
45+
)
46+
if len(preferred_compilers)>1:
47+
raise Exception(f"Invalid value for packages:fortran:prefer is {preferred_compilers}")
48+
preferred_compiler = preferred_compilers[0]
49+
return preferred_compiler
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
3+
import copy
4+
import logging
5+
import os
6+
import re
7+
import sys
8+
9+
import spack
10+
import spack.environment as ev
11+
from spack.provider_index import ProviderIndex
12+
13+
from spack.extensions.stack.common import ALIASES
14+
from spack.extensions.stack.common import RED, RESET
15+
from spack.extensions.stack.common import get_preferred_compiler
16+
17+
18+
def get_compiler_name_and_version(string):
19+
compiler_name = string.replace("@=", "@").split("@")[0]
20+
try:
21+
compiler_version = string.replace("@=", "@").split("@")[1]
22+
except:
23+
compiler_version = None
24+
return (compiler_name, compiler_version)
25+
26+
27+
def get_compiler_choice(string):
28+
"""Parse string for a Spack version 1 compiler dependency
29+
declaration. By intentionally not matching old (spack v0)
30+
compiler dependency declarations ("%gcc", "%oneapi", ...),
31+
we force updating the Spack configuration files to v1."""
32+
COMPILER_CHOICE_REGEX_STRING = "^(%+)(" + \
33+
"c=|" + \
34+
"cxx=|" + \
35+
"fortran=|" + \
36+
"c,cxx=|" + \
37+
"cxx,c=|" + \
38+
"c,fortran=|" + \
39+
"fortran,c=|" + \
40+
"cxx,fortran=|" + \
41+
"fortran,cxx=|" + \
42+
"c,cxx,fortran=|" + \
43+
"fortran,c,cxx=|" + \
44+
"cxx,fortran,c=|" + \
45+
"c,fortran,cxx=|" + \
46+
"cxx,c,fortran=|" + \
47+
"fortran,cxx,c=)(\S+)\s*$"
48+
COMPILER_CHOICE_REGEX = re.compile(COMPILER_CHOICE_REGEX_STRING)
49+
match = COMPILER_CHOICE_REGEX.match(string)
50+
if match:
51+
return match.group(3)
52+
return None
53+
54+
55+
def check_preferred_compiler():
56+
"""For an active environment, check that the preferred compiler
57+
is being used for all packages except those that explicitly
58+
request a different compiler. For the latter packages, check
59+
that the explicitly requested compiler is being used."""
60+
61+
logging.info("Configuring active spack environment ...")
62+
env_dir = ev.active_environment().path
63+
if not env_dir:
64+
raise Exception("No active spack environment")
65+
env = spack.environment.Environment(env_dir)
66+
spack.environment.environment.activate(env)
67+
logging.info(" ... environment directory: {}".format(env_dir))
68+
69+
# Get all specs and determine compilers
70+
specs = env.all_specs()
71+
if not specs:
72+
raise Exception(f"{RED}No specs found - did you run 'spack concretize'?{RESET}")
73+
q = ProviderIndex(specs=specs, repository=spack.repo.PATH)
74+
75+
c_providers = q.providers_for("c")
76+
cxx_providers = q.providers_for("cxx")
77+
fortran_providers = q.providers_for("fortran")
78+
compilers = list(set(c_providers + cxx_providers + fortran_providers))
79+
if not compilers:
80+
raise Exception(f"{RED}No compilers found{RESET}!")
81+
logging.info(f" ... compilers: {compilers}")
82+
83+
# Determine the preferred compiler
84+
preferred_compiler = get_preferred_compiler(spack.config)
85+
(preferred_compiler_name, preferred_compiler_version) = get_compiler_name_and_version(preferred_compiler)
86+
logging.info(" ... preferred compiler: {}".format(preferred_compiler))
87+
88+
# Get package config to compare actual specs against the intended config
89+
package_config = spack.config.get("packages")
90+
91+
logging.info("Checking all specs ...")
92+
errors = 0
93+
for spec in specs:
94+
# If the spec has no compiler dependency, an exception will be thrown - ignore package
95+
try:
96+
compiler_name = spec.compiler.name
97+
compiler_version = spec.compiler.version if preferred_compiler_version else None
98+
except:
99+
logging.info(f" ... {spec.name}@{spec.version}/{spec.dag_hash(length=7)} has no compiler dependency")
100+
continue
101+
# If the spec compiler matches the preferred compiler for the environment, move on.
102+
# Note that this permits situations where a packages has an explicit preferred (but
103+
# not explicitly required) compiler, but Spack decides to use the preferred (and
104+
# different) compiler for the environment instead.
105+
if preferred_compiler_name == compiler_name and preferred_compiler_version == compiler_version:
106+
logging.info(f" ... {spec.name}@{spec.version}/{spec.dag_hash(length=7)} uses preferred compiler")
107+
else:
108+
spec_required_compiler_name = None
109+
spec_required_compiler_version = None
110+
spec_preferred_compiler_name = None
111+
spec_preferred_compiler_version = None
112+
for key, value in package_config[spec.name].items():
113+
# To simplify parsing, turn scalar values into CommentedSeq of length 1
114+
if isinstance(value, (str, bytes)):
115+
values = CommentedSeq([value])
116+
else:
117+
values = value
118+
# Loop through all values to check for required or preferred compilers
119+
for entry in values:
120+
if key.lower() == "require":
121+
choice = get_compiler_choice(entry.lower())
122+
# Not a compiler preference, carry on
123+
if not choice:
124+
continue
125+
# Check that the explicitly required compiler is a valid (existing)
126+
# compiler for this environment. This requirement may be relaxed in
127+
# the future if we start building compilers in spack environments.
128+
if any(choice in c for c in compilers):
129+
(spec_required_compiler_name, spec_required_compiler_version) = get_compiler_name_and_version(choice)
130+
elif key.lower() == "prefer":
131+
choice = get_compiler_choice(entry.lower())
132+
# Not a compiler preference, carry on
133+
if not choice:
134+
continue
135+
# Check that the explicitly preferred compiler is a valid (existing)
136+
# compiler for this environment. This requirement may be relaxed in
137+
# the future if we start building compilers in spack environments.
138+
if any(choice in c for c in compilers):
139+
(spec_preferred_compiler_name, spec_preferred_compiler_version) = get_compiler_name_and_version(choice)
140+
# If we have a hard requirement for a compiler, we can stop scanning the spec package config
141+
if spec_required_compiler_name:
142+
break
143+
if spec_required_compiler_name == compiler_name and \
144+
( (not spec_required_compiler_version or not compiler_version) or \
145+
(spec_required_compiler_version==compiler_version) ):
146+
logging.info(f" ... {spec.name}@{spec.version}/{spec.dag_hash(length=7)} uses explicitly required compiler")
147+
elif spec_preferred_compiler_name == compiler_name and \
148+
( (not spec_preferred_compiler_version or not compiler_version) or \
149+
(spec_preferred_compiler_version==compiler_version) ):
150+
logging.info(f" ... {spec.name}@{spec.version}/{spec.dag_hash(length=7)} uses explicitly preferred compiler")
151+
else:
152+
errors += 1
153+
logging.error(f" ... {RED}error: {spec.name}@{spec.version}/{spec.dag_hash(length=7)} does not use intended compiler\n" + \
154+
f" check also that any explicit preferred/required compiler dependencies are using Spack v1 syntax{RESET}")
155+
if errors:
156+
raise Exception(f"{RED}Detected {errors} compiler mismatches!{RESET}")

spack-ext/lib/jcsda-emc/spack-stack/stack/meta_modules.py

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
import spack
1010
import spack.environment as ev
11-
# import spack.repo
1211
from spack.provider_index import ProviderIndex
1312

13+
from spack.extensions.stack.common import ALIASES
14+
from spack.extensions.stack.common import get_preferred_compiler
15+
1416
# logging.basicConfig(level=logging.INFO)
1517
logging.basicConfig(format="%(message)s", level=logging.DEBUG)
1618

@@ -52,24 +54,6 @@
5254
"MPIROOT": "",
5355
}
5456

55-
# Aliases to shorten module paths for tcl modules. These aliases must match
56-
# the compiler and MPI name translations in configs/common/modules_tcl.yaml
57-
ALIASES = {
58-
"none" : "none",
59-
# Compilers
60-
"gcc" : "gcc",
61-
"intel-oneapi-compilers-classic" : "intel",
62-
"intel-oneapi-compilers" : "oneapi",
63-
"llvm" : "llvm",
64-
# MPI
65-
"cray-mpich" : "cray-mpich",
66-
# Do we still need intel-mpi, and if yes, use the same impi?
67-
"intel-oneapi-mpi" : "impi",
68-
"mpich" : "mpich",
69-
"mpt" : "mpt",
70-
"openmpi" : "openmpi",
71-
}
72-
7357

7458
def setenv_command(module_choice, key, value):
7559
if module_choice == "lmod":
@@ -172,26 +156,6 @@ def substitute_config_vars(config_str):
172156
return config_str
173157

174158

175-
def get_preferred_compiler():
176-
"""Determine the preferred compiler by looking at
177-
packages:
178-
fortran:
179-
prefer:
180-
- COMPILER_NAME (gcc, intel-oneapi-compilers, llvm, ..)
181-
"""
182-
try:
183-
preferred_compilers = spack.config.get("packages")["fortran"]["prefer"]
184-
except:
185-
raise Exception(
186-
"""Unable to detect preferred compiler from environment.
187-
Does the environment have the config entry 'packages:fortran:prefer?'"""
188-
)
189-
if len(preferred_compilers)>1:
190-
raise Exception(f"Invalid value for packages:fortran:prefer is {preferred_compilers}")
191-
preferred_compiler = preferred_compilers[0]
192-
return preferred_compiler
193-
194-
195159
def remove_compiler_prefices_from_tcl_modulefiles(modulepath, compiler_list, mpi_provider, module_choice):
196160
"""Remove compiler and mpi prefices from tcl modulefiles in modulepath"""
197161
logging.info(f" ... ... removing compiler/mpi prefices from tcl modulefiles in {modulepath}")
@@ -243,7 +207,12 @@ def remove_compiler_prefices_from_tcl_modulefiles(modulepath, compiler_list, mpi
243207

244208

245209
def setup_meta_modules():
246-
# Find currently active spack environment, activate here
210+
"""For an active environment, create meta-modules for the preferred
211+
compiler and the MPI provider (compiled with the preferred compiler).
212+
For tcl/tk environment modules, remove modulepath prefices from the
213+
spack-generated modules and implement a module hiearchy modeled after
214+
the lua/lmod modules."""
215+
247216
logging.info("Configuring active spack environment ...")
248217
env_dir = ev.active_environment().path
249218
if not env_dir:
@@ -282,8 +251,7 @@ def setup_meta_modules():
282251
logging.info(f" ... module directory: {module_dir}")
283252

284253
# Get all specs and determine compilers
285-
hashes = env.all_hashes()
286-
specs = spack.store.STORE.db.query(hashes=hashes)
254+
specs = env.all_specs()
287255
q = ProviderIndex(specs=specs, repository=spack.repo.PATH)
288256

289257
c_providers = q.providers_for("c")
@@ -320,7 +288,7 @@ def setup_meta_modules():
320288
# takes it and adds it to the stack-COMPILER metamodule. Likewise, we need
321289
# to save the list of compiler substitutions from the preferred compiler
322290
# so that we have access to it when we build the MPI meta module.
323-
preferred_compiler = get_preferred_compiler()
291+
preferred_compiler = get_preferred_compiler(spack.config)
324292
logging.info(" ... preferred compiler: {}".format(preferred_compiler))
325293

326294
# Sort the list using a custom key
@@ -330,7 +298,7 @@ def custom_sort_key(entry):
330298
return (1 if preferred_compiler in entry else 0, entry)
331299
compilers = sorted(compilers, key=custom_sort_key)
332300

333-
# Get mpi providers (currently only one mpi provider is supported)
301+
# Get mpi providers (currently only one mpi provider is supported)
334302
mpi_providers = q.providers_for("mpi")
335303
if len(mpi_providers)>1:
336304
raise Exception(f"Expected no or one MPI provider, but got {mpi_providers}")

0 commit comments

Comments
 (0)