Skip to content

Commit e9fcef8

Browse files
authored
Merge pull request #4669 from bartoldeman/job-local-tweak
Let jobs retweak easyconfigs themselves by passing down `--try-*` options
2 parents 2e8e993 + ec5e7c0 commit e9fcef8

File tree

8 files changed

+93
-32
lines changed

8 files changed

+93
-32
lines changed

easybuild/framework/easyconfig/tweak.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* Fotis Georgatos (Uni.Lu, NTUA)
3737
* Alan O'Cais (Juelich Supercomputing Centre)
3838
* Maxime Boissonneault (Universite Laval, Calcul Quebec, Compute Canada)
39+
* Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
3940
"""
4041
import copy
4142
import functools
@@ -82,8 +83,9 @@ def ec_filename_for(path):
8283
return fn
8384

8485

85-
def tweak(easyconfigs, build_specs, modtool, targetdirs=None):
86-
"""Tweak list of easyconfigs according to provided build specifications."""
86+
def tweak(easyconfigs, build_specs, modtool, targetdirs=None, return_map=False):
87+
"""Tweak list of easyconfigs according to provided build specifications.
88+
If return_map=True, also returns tweaked to original file mapping"""
8789
# keep track of originally listed easyconfigs (via their path)
8890
listed_ec_paths = [ec['spec'] for ec in easyconfigs]
8991

@@ -92,6 +94,7 @@ def tweak(easyconfigs, build_specs, modtool, targetdirs=None):
9294
tweaked_ecs_path, tweaked_ecs_deps_path = targetdirs
9395
modifying_toolchains_or_deps = False
9496
src_to_dst_tc_mapping = {}
97+
tweak_map = {}
9598
revert_to_regex = False
9699

97100
if 'update_deps' in build_specs:
@@ -223,13 +226,18 @@ def tweak(easyconfigs, build_specs, modtool, targetdirs=None):
223226
if modifying_toolchains_or_deps:
224227
if tc_name in src_to_dst_tc_mapping:
225228
# Note pruned_build_specs are not passed down for dependencies
226-
map_easyconfig_to_target_tc_hierarchy(orig_ec['spec'], src_to_dst_tc_mapping,
227-
targetdir=tweaked_ecs_deps_path,
228-
update_dep_versions=update_dependencies,
229-
ignore_versionsuffixes=ignore_versionsuffixes)
229+
new_ec_file = map_easyconfig_to_target_tc_hierarchy(orig_ec['spec'], src_to_dst_tc_mapping,
230+
targetdir=tweaked_ecs_deps_path,
231+
update_dep_versions=update_dependencies,
232+
ignore_versionsuffixes=ignore_versionsuffixes)
230233
else:
231-
tweak_one(orig_ec['spec'], None, build_specs, targetdir=tweaked_ecs_deps_path)
234+
new_ec_file = tweak_one(orig_ec['spec'], None, build_specs, targetdir=tweaked_ecs_deps_path)
235+
236+
if new_ec_file:
237+
tweak_map[new_ec_file] = orig_ec['spec']
232238

239+
if return_map:
240+
return tweaked_easyconfigs, tweak_map
233241
return tweaked_easyconfigs
234242

235243

easybuild/main.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
* Ward Poelmans (Ghent University)
3838
* Fotis Georgatos (Uni.Lu, NTUA)
3939
* Maxime Boissonneault (Compute Canada)
40+
* Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
4041
"""
4142
import copy
4243
import os
@@ -430,7 +431,9 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
430431
# don't try and tweak anything if easyconfigs were generated, since building a full dep graph will fail
431432
# if easyconfig files for the dependencies are not available
432433
if try_to_generate and build_specs and not generated_ecs:
433-
easyconfigs = tweak(easyconfigs, build_specs, modtool, targetdirs=tweaked_ecs_paths)
434+
easyconfigs, tweak_map = tweak(easyconfigs, build_specs, modtool, targetdirs=tweaked_ecs_paths, return_map=True)
435+
else:
436+
tweak_map = None
434437

435438
if options.containerize:
436439
# if --containerize/-C create a container recipe (and optionally container image), and stop
@@ -552,7 +555,7 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
552555

553556
# submit build as job(s), clean up and exit
554557
if options.job:
555-
submit_jobs(ordered_ecs, eb_go.generate_cmd_line(), testing=testing)
558+
submit_jobs(ordered_ecs, eb_go.generate_cmd_line(), testing=testing, tweak_map=tweak_map)
556559
if not testing:
557560
print_msg("Submitted parallel build jobs, exiting now")
558561
return True

easybuild/tools/job/backend.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"""
3333

3434
from abc import ABCMeta, abstractmethod
35+
from types import SimpleNamespace
3536

3637
from easybuild.base import fancylogger
3738
from easybuild.tools.config import get_job_backend
@@ -69,7 +70,7 @@ def make_job(self, script, name, env_vars=None, hours=None, cores=None):
6970
See the `Job`:class: constructor for an explanation of what
7071
the arguments are.
7172
"""
72-
pass
73+
return SimpleNamespace()
7374

7475
@abstractmethod
7576
def queue(self, job, dependencies=frozenset()):

easybuild/tools/parallelbuild.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* Toon Willems (Ghent University)
3434
* Kenneth Hoste (Ghent University)
3535
* Stijn De Weirdt (Ghent University)
36+
* Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
3637
"""
3738
import math
3839
import os
@@ -45,7 +46,7 @@
4546
from easybuild.tools.config import build_option, get_repository, get_repositorypath
4647
from easybuild.tools.filetools import get_cwd
4748
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
48-
from easybuild.tools.job.backend import job_backend
49+
from easybuild.tools.job.backend import job_backend, JobBackend
4950
from easybuild.tools.repository.repository import init_repository
5051

5152

@@ -57,7 +58,8 @@ def _to_key(dep):
5758
return ActiveMNS().det_full_module_name(dep)
5859

5960

60-
def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybuild-build', prepare_first=True):
61+
def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybuild-build', testing=False,
62+
prepare_first=True, tweak_map=None, try_opts=''):
6163
"""
6264
Build easyconfigs in parallel by submitting jobs to a batch-queuing system.
6365
Return list of jobs submitted.
@@ -69,11 +71,14 @@ def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybu
6971
:param build_command: build command to use
7072
:param easyconfigs: list of easyconfig files
7173
:param output_dir: output directory
74+
:param testing: If `True`, skip actual job submission
7275
:param prepare_first: prepare by runnning fetch step first for each easyconfig
76+
:param tweak_map: Mapping from tweaked to original easyconfigs
77+
:param try_opts: --try-* options to pass if the easyconfig is tweaked
7378
"""
7479
_log.info("going to build these easyconfigs in parallel: %s", [os.path.basename(ec['spec']) for ec in easyconfigs])
7580

76-
active_job_backend = job_backend()
81+
active_job_backend = JobBackend() if testing else job_backend()
7782
if active_job_backend is None:
7883
raise EasyBuildError("Can not use --job if no job backend is available.")
7984

@@ -93,12 +98,17 @@ def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybu
9398
# this is very important, otherwise we might have race conditions
9499
# e.g. GCC-4.5.3 finds cloog.tar.gz but it was incorrectly downloaded by GCC-4.6.3
95100
# running this step here, prevents this
96-
if prepare_first:
101+
if prepare_first and not testing:
97102
prepare_easyconfig(easyconfig)
98103

104+
# convert <tweaked easyconfig.eb> to <original-easyconfig.eb --try-xxx> to avoid needing a shared tmpdir
105+
spec = easyconfig['spec']
106+
if spec in (tweak_map or {}):
107+
spec = tweak_map[spec] + try_opts
108+
99109
# the new job will only depend on already submitted jobs
100-
_log.info("creating job for ec: %s" % os.path.basename(easyconfig['spec']))
101-
new_job = create_job(active_job_backend, build_command, easyconfig, output_dir=output_dir)
110+
_log.info("creating job for ec: %s using %s" % (os.path.basename(easyconfig['spec']), spec))
111+
new_job = create_job(active_job_backend, build_command, easyconfig, output_dir=output_dir, spec=spec)
102112

103113
# filter out dependencies marked as external modules
104114
deps = [d for d in easyconfig['ec'].all_dependencies if not d.get('external_module', False)]
@@ -116,24 +126,27 @@ def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybu
116126

117127
active_job_backend.complete()
118128

119-
return jobs
129+
return build_command if testing else jobs
120130

121131

122-
def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True):
132+
def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True, tweak_map=None):
123133
"""
124134
Submit jobs.
125135
:param ordered_ecs: list of easyconfigs, in the order they should be processed
126136
:param cmd_line_opts: list of command line options (in 'longopt=value' form)
127137
:param testing: If `True`, skip actual job submission
128138
:param prepare_first: prepare by runnning fetch step first for each easyconfig
139+
:param tweak_map: Mapping from tweaked to original easyconfigs
129140
"""
130141
curdir = get_cwd()
131142

132-
# regex pattern for options to ignore (help options can't reach here)
143+
# regex patterns for options to ignore and tweak options (help options can't reach here)
133144
ignore_opts = re.compile('^--robot$|^--job|^--try-.*$|^--easystack$')
145+
try_opts_re = re.compile('^--try-.*$')
134146

135147
# generate_cmd_line returns the options in form --longopt=value
136148
opts = [o for o in cmd_line_opts if not ignore_opts.match(o.split('=')[0])]
149+
try_opts = [o for o in cmd_line_opts if try_opts_re.match(o.split('=')[0])]
137150

138151
# add --disable-job to make sure the submitted job doesn't submit a job itself,
139152
# resulting in an infinite cycle of jobs;
@@ -143,6 +156,7 @@ def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True):
143156

144157
# compose string with command line options, properly quoted and with '%' characters escaped
145158
opts_str = ' '.join(opts).replace('%', '%%')
159+
try_opts_str = ' ' + ' '.join(try_opts).replace('%', '%%')
146160

147161
eb_cmd = build_option('job_eb_cmd')
148162

@@ -154,19 +168,19 @@ def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True):
154168
_log.info("Command template for jobs: %s", command)
155169
if testing:
156170
_log.debug("Skipping actual submission of jobs since testing mode is enabled")
157-
return command
158-
else:
159-
return build_easyconfigs_in_parallel(command, ordered_ecs, prepare_first=prepare_first)
171+
return build_easyconfigs_in_parallel(command, ordered_ecs, testing=testing, prepare_first=prepare_first,
172+
tweak_map=tweak_map, try_opts=try_opts_str)
160173

161174

162-
def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-build'):
175+
def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-build', spec=''):
163176
"""
164177
Creates a job to build a *single* easyconfig.
165178
166179
:param job_backend: A factory object for querying server parameters and creating actual job objects
167180
:param build_command: format string for command, full path to an easyconfig file will be substituted in it
168181
:param easyconfig: easyconfig as processed by process_easyconfig
169182
:param output_dir: optional output path; --regtest-output-dir will be used inside the job with this variable
183+
:param spec: untweaked easyconfig name with optional --try-* options
170184
171185
returns the job
172186
"""
@@ -183,7 +197,7 @@ def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-bui
183197
command = build_command % {
184198
'add_opts': add_opts,
185199
'output_dir': os.path.join(os.path.abspath(output_dir), name),
186-
'spec': easyconfig['spec'],
200+
'spec': spec or easyconfig['spec'],
187201
}
188202

189203
# just use latest build stats

test/framework/easyconfig.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,8 @@ def test_tweak_multiple_tcs(self):
805805
untweaked_openmpi_2 = os.path.join(test_easyconfigs, 'o', 'OpenMPI', 'OpenMPI-3.1.1-GCC-7.3.0-2.30.eb')
806806
easyconfigs, _ = parse_easyconfigs([(untweaked_openmpi_1, False), (untweaked_openmpi_2, False)])
807807
tweak_specs = {'moduleclass': 'debugger'}
808-
easyconfigs = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths)
808+
easyconfigs, tweak_map = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths,
809+
return_map=True)
809810
# Check that all expected tweaked easyconfigs exists
810811
tweaked_openmpi_1 = os.path.join(tweaked_ecs_paths[0], os.path.basename(untweaked_openmpi_1))
811812
tweaked_openmpi_2 = os.path.join(tweaked_ecs_paths[0], os.path.basename(untweaked_openmpi_2))
@@ -817,6 +818,7 @@ def test_tweak_multiple_tcs(self):
817818
"Tweaked value not found in " + tweaked_openmpi_content_1)
818819
self.assertTrue('moduleclass = "debugger"' in tweaked_openmpi_content_2,
819820
"Tweaked value not found in " + tweaked_openmpi_content_2)
821+
self.assertEqual(tweak_map, {tweaked_openmpi_1: untweaked_openmpi_1, tweaked_openmpi_2: untweaked_openmpi_2})
820822

821823
def test_installversion(self):
822824
"""Test generation of install version."""

test/framework/options.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,9 +508,10 @@ def test_job(self):
508508
"""Test submitting build as a job."""
509509

510510
# use gzip-1.4.eb easyconfig file that comes with the tests
511-
eb_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 'g', 'gzip', 'gzip-1.4.eb')
511+
test_ecs = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs')
512+
eb_file = os.path.join(test_ecs, 'g', 'gzip', 'gzip-1.4.eb')
512513

513-
def check_args(job_args, passed_args=None):
514+
def check_args(job_args, passed_args=None, msgstrs=None, try_opts='', tweaked_eb_file='gzip-1.4.eb'):
514515
"""Check whether specified args yield expected result."""
515516
if passed_args is None:
516517
passed_args = job_args[:]
@@ -529,16 +530,43 @@ def check_args(job_args, passed_args=None):
529530
assertmsg = "Info log msg with job command template for --job (job_msg: %s, outtxt: %s)" % (job_msg, outtxt)
530531
self.assertTrue(re.search(job_msg, outtxt), assertmsg)
531532

533+
if msgstrs is None:
534+
msgstrs = [(tweaked_eb_file, eb_file + try_opts)]
535+
536+
assertmsg = "Info log msg with creating job for --job (job_msg: %s, outtxt: %s)" % (job_msg, outtxt)
537+
for msgstr in msgstrs:
538+
job_msg = r"INFO creating job for ec: %s using %s\n" % msgstr
539+
self.assertTrue(re.search(job_msg, outtxt), assertmsg)
540+
532541
# options passed are reordered, so order here matters to make tests pass
533542
check_args(['--debug'])
534543
check_args(['--debug', '--stop=configure', '--try-software-name=foo'],
535-
passed_args=['--debug', "--stop='configure'"])
544+
passed_args=['--debug', "--stop='configure'"],
545+
try_opts=" --try-software-name='foo'",
546+
tweaked_eb_file="foo-1.4.eb")
536547
check_args(['--debug', '--robot-paths=/tmp/foo:/tmp/bar'],
537548
passed_args=['--debug', "--robot-paths='/tmp/foo:/tmp/bar'"])
538549
# --robot has preference over --robot-paths, --robot is not passed down
539550
check_args(['--debug', '--robot-paths=/tmp/foo', '--robot=%s' % self.test_prefix],
540551
passed_args=['--debug', "--robot-paths='%s:/tmp/foo'" % self.test_prefix])
541552

553+
# check if libtoy dep uses --try-toolchain but gzip does not (easyconfig exists already)
554+
eb_file = os.path.join(self.test_buildpath, 'toy-0.0-with-deps.eb')
555+
copy_file(os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb'), eb_file)
556+
write_file(eb_file, "dependencies = [('libtoy', '0.0'), ('gzip', '1.4')]\n", append=True)
557+
try_opts = " --try-toolchain='GCC,4.9.3-2.26'"
558+
tweaked_eb_file = "toy-0.0-GCC-4.9.3-2.26.eb"
559+
gzip_eb_file = 'gzip-1.4-GCC-4.9.3-2.26.eb'
560+
check_args(['--debug', '--stop=configure', '--try-toolchain=GCC,4.9.3-2.26', '--robot'],
561+
passed_args=['--debug', "--stop='configure'"],
562+
msgstrs=[
563+
(tweaked_eb_file, eb_file + try_opts),
564+
('libtoy-0.0-GCC-4.9.3-2.26.eb',
565+
os.path.join(test_ecs, 'l', 'libtoy', 'libtoy-0.0.eb') + try_opts),
566+
(gzip_eb_file, os.path.join(test_ecs, 'g', 'gzip', gzip_eb_file))],
567+
try_opts=try_opts,
568+
tweaked_eb_file=tweaked_eb_file)
569+
542570
# 'zzz' prefix in the test name is intentional to make this test run last,
543571
# since it fiddles with the logging infrastructure which may break things
544572
def test_zzz_logtostdout(self):

test/framework/parallelbuild.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def test_build_easyconfigs_in_parallel_gc3pie(self):
290290
def test_submit_jobs(self):
291291
"""Test submit_jobs"""
292292
test_easyconfigs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
293-
toy_ec = os.path.join(test_easyconfigs_dir, 't', 'toy', 'toy-0.0.eb')
293+
toy_ec = process_easyconfig(os.path.join(test_easyconfigs_dir, 't', 'toy', 'toy-0.0.eb'))
294294

295295
args = [
296296
'--debug',
@@ -303,7 +303,7 @@ def test_submit_jobs(self):
303303
'--job-cores=3',
304304
]
305305
eb_go = parse_options(args=args)
306-
cmd = submit_jobs([toy_ec], eb_go.generate_cmd_line(), testing=True)
306+
cmd = submit_jobs(toy_ec, eb_go.generate_cmd_line(), testing=True)
307307

308308
# these patterns must be found
309309
regexs = [
@@ -331,7 +331,7 @@ def test_submit_jobs(self):
331331

332332
# test again with custom EasyBuild command to use in jobs
333333
update_build_option('job_eb_cmd', "/just/testing/bin/eb --debug")
334-
cmd = submit_jobs([toy_ec], eb_go.generate_cmd_line(), testing=True)
334+
cmd = submit_jobs(toy_ec, eb_go.generate_cmd_line(), testing=True)
335335
regex = re.compile(r" && /just/testing/bin/eb --debug %\(spec\)s ")
336336
self.assertTrue(regex.search(cmd), "Pattern '%s' found in: %s" % (regex.pattern, cmd))
337337

test/framework/robot.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1139,7 +1139,8 @@ def test_tweak_robotpath(self):
11391139

11401140
# Tweak the toolchain version of the easyconfig
11411141
tweak_specs = {'toolchain_version': '6.4.0-2.28'}
1142-
easyconfigs = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths)
1142+
easyconfigs, tweak_map = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths,
1143+
return_map=True)
11431144

11441145
# Check that all expected tweaked easyconfigs exists
11451146
tweaked_openmpi = os.path.join(tweaked_ecs_paths[0], 'OpenMPI-2.1.2-GCC-6.4.0-2.28.eb')
@@ -1155,6 +1156,10 @@ def test_tweak_robotpath(self):
11551156
# Check it picks up the untweaked dependency of the tweaked OpenMPI
11561157
untweaked_hwloc = os.path.join(test_easyconfigs, 'h', 'hwloc', 'hwloc-1.11.8-GCC-6.4.0-2.28.eb')
11571158
self.assertIn(untweaked_hwloc, specs)
1159+
# Check correctness of tweak_map (maps back to the original untweaked file, even for hwloc, where the
1160+
# tweaked version is generated but not used)
1161+
self.assertEqual(tweak_map, {tweaked_openmpi: untweaked_openmpi,
1162+
tweaked_hwloc: untweaked_hwloc.replace("6.4.0-2.28", "4.6.4")})
11581163

11591164
def test_robot_find_subtoolchain_for_dep(self):
11601165
"""Test robot_find_subtoolchain_for_dep."""

0 commit comments

Comments
 (0)