Skip to content

Commit a2550eb

Browse files
authored
Merge pull request #4534 from dagonzalezfo/4426_granular_exit_code
More granular exit codes
2 parents 7d93348 + 9eaec3b commit a2550eb

File tree

19 files changed

+696
-334
lines changed

19 files changed

+696
-334
lines changed

easybuild/framework/easyblock.py

Lines changed: 152 additions & 118 deletions
Large diffs are not rendered by default.

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
from easybuild.framework.easyconfig.templates import ALTERNATIVE_EASYCONFIG_TEMPLATES, DEPRECATED_EASYCONFIG_TEMPLATES
6666
from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS, TEMPLATE_NAMES_DYNAMIC, template_constant_dict
6767
from easybuild.tools import LooseVersion
68-
from easybuild.tools.build_log import EasyBuildError, print_warning, print_msg
68+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_warning, print_msg
6969
from easybuild.tools.config import GENERIC_EASYBLOCK_PKG, LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG
7070
from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN
7171
from easybuild.tools.config import Singleton, build_option, get_module_naming_scheme
@@ -907,9 +907,12 @@ def validate_os_deps(self):
907907
not_found.append(dep)
908908

909909
if not_found:
910-
raise EasyBuildError("One or more OS dependencies were not found: %s", not_found)
911-
else:
912-
self.log.info("OS dependencies ok: %s" % self['osdependencies'])
910+
raise EasyBuildError(
911+
"One or more OS dependencies were not found: %s", not_found,
912+
exit_code=EasyBuildExit.MISSING_SYSTEM_DEPENDENCY
913+
)
914+
915+
self.log.info("OS dependencies ok: %s" % self['osdependencies'])
913916

914917
return True
915918

@@ -1272,7 +1275,10 @@ def _validate(self, attr, values): # private method
12721275
if values is None:
12731276
values = []
12741277
if self[attr] and self[attr] not in values:
1275-
raise EasyBuildError("%s provided '%s' is not valid: %s", attr, self[attr], values)
1278+
raise EasyBuildError(
1279+
"%s provided '%s' is not valid: %s", attr, self[attr], values,
1280+
exit_code=EasyBuildExit.VALUE_ERROR
1281+
)
12761282

12771283
def probe_external_module_metadata(self, mod_name, existing_metadata=None):
12781284
"""
@@ -1922,12 +1928,20 @@ def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error
19221928
error_re = re.compile(r"No module named '?.*/?%s'?" % modname)
19231929
_log.debug("error regexp for ImportError on '%s' easyblock: %s", modname, error_re.pattern)
19241930
if error_re.match(str(err)):
1931+
# Missing easyblock type of error
19251932
if error_on_missing_easyblock:
1926-
raise EasyBuildError("No software-specific easyblock '%s' found for %s", class_name, name)
1927-
elif error_on_failed_import:
1928-
raise EasyBuildError("Failed to import %s easyblock: %s", class_name, err)
1933+
raise EasyBuildError(
1934+
"No software-specific easyblock '%s' found for %s", class_name, name,
1935+
exit_code=EasyBuildExit.MISSING_EASYBLOCK
1936+
) from err
19291937
else:
1930-
_log.debug("Failed to import easyblock for %s, but ignoring it: %s" % (class_name, err))
1938+
# Broken import
1939+
if error_on_failed_import:
1940+
raise EasyBuildError(
1941+
"Failed to import %s easyblock: %s", class_name, err,
1942+
exit_code=EasyBuildExit.EASYBLOCK_ERROR
1943+
) from err
1944+
_log.debug("Failed to import easyblock for %s, but ignoring it: %s" % (class_name, err))
19311945

19321946
if cls is not None:
19331947
_log.info("Successfully obtained class '%s' for easyblock '%s' (software name '%s')",
@@ -1941,7 +1955,10 @@ def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error
19411955
# simply reraise rather than wrapping it into another error
19421956
raise err
19431957
except Exception as err:
1944-
raise EasyBuildError("Failed to obtain class for %s easyblock (not available?): %s", easyblock, err)
1958+
raise EasyBuildError(
1959+
"Failed to obtain class for %s easyblock (not available?): %s", easyblock, err,
1960+
exit_code=EasyBuildExit.EASYBLOCK_ERROR
1961+
)
19451962

19461963

19471964
def get_module_path(name, generic=None, decode=True):
@@ -2086,7 +2103,11 @@ def process_easyconfig(path, build_specs=None, validate=True, parse_only=False,
20862103
try:
20872104
ec = EasyConfig(spec, build_specs=build_specs, validate=validate, hidden=hidden)
20882105
except EasyBuildError as err:
2089-
raise EasyBuildError("Failed to process easyconfig %s: %s", spec, err.msg)
2106+
try:
2107+
exit_code = err.exit_code
2108+
except AttributeError:
2109+
exit_code = EasyBuildExit.EASYCONFIG_ERROR
2110+
raise EasyBuildError("Failed to process easyconfig %s: %s", spec, err.msg, exit_code=exit_code)
20902111

20912112
name = ec['name']
20922113

easybuild/framework/easyconfig/tools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from easybuild.framework.easyconfig.easyconfig import process_easyconfig
5555
from easybuild.framework.easyconfig.style import cmdline_easyconfigs_style_check
5656
from easybuild.tools import LooseVersion
57-
from easybuild.tools.build_log import EasyBuildError, print_error, print_msg, print_warning
57+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_error, print_msg, print_warning
5858
from easybuild.tools.config import build_option
5959
from easybuild.tools.environment import restore_env
6060
from easybuild.tools.filetools import find_easyconfigs, get_cwd, is_patch_file, locate_files
@@ -409,7 +409,7 @@ def parse_easyconfigs(paths, validate=True):
409409
# keep track of whether any files were generated
410410
generated_ecs |= generated
411411
if not os.path.exists(path):
412-
raise EasyBuildError("Can't find path %s", path)
412+
raise EasyBuildError("Can't find path %s", path, exit_code=EasyBuildExit.MISSING_EASYCONFIG)
413413
try:
414414
ec_files = find_easyconfigs(path, ignore_dirs=build_option('ignore_dirs'))
415415
for ec_file in ec_files:

easybuild/framework/extension.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
from easybuild.framework.easyconfig.easyconfig import resolve_template
4242
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
43-
from easybuild.tools.build_log import EasyBuildError, raise_nosupport
43+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, raise_nosupport
4444
from easybuild.tools.filetools import change_dir
4545
from easybuild.tools.run import run_shell_cmd
4646

@@ -302,7 +302,7 @@ def sanity_check_step(self):
302302
cmd, stdin = resolve_exts_filter_template(exts_filter, self)
303303
cmd_res = run_shell_cmd(cmd, fail_on_error=False, stdin=stdin)
304304

305-
if cmd_res.exit_code:
305+
if cmd_res.exit_code != EasyBuildExit.SUCCESS:
306306
if stdin:
307307
fail_msg = 'command "%s" (stdin: "%s") failed' % (cmd, stdin)
308308
else:

easybuild/main.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,10 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
133133

134134
ec_res = {}
135135
try:
136-
(ec_res['success'], app_log, err) = build_and_install_one(ec, init_env)
136+
(ec_res['success'], app_log, err_msg, err_code) = build_and_install_one(ec, init_env)
137137
ec_res['log_file'] = app_log
138138
if not ec_res['success']:
139-
ec_res['err'] = EasyBuildError(err)
139+
ec_res['err'] = EasyBuildError(err_msg, exit_code=err_code)
140140
except Exception as err:
141141
# purposely catch all exceptions
142142
ec_res['success'] = False
@@ -174,7 +174,7 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
174174
if not isinstance(ec_res['err'], EasyBuildError):
175175
raise ec_res['err']
176176
else:
177-
raise EasyBuildError(test_msg)
177+
raise EasyBuildError(test_msg, exit_code=err_code)
178178

179179
res.append((ec, ec_res))
180180

@@ -779,15 +779,15 @@ def main_with_hooks(args=None):
779779
try:
780780
init_session_state, eb_go, cfg_settings = prepare_main(args=args)
781781
except EasyBuildError as err:
782-
print_error(err.msg)
782+
print_error(err.msg, exit_code=err.exit_code)
783783

784784
hooks = load_hooks(eb_go.options.hooks)
785785

786786
try:
787787
main(args=args, prepared_cfg_data=(init_session_state, eb_go, cfg_settings))
788788
except EasyBuildError as err:
789789
run_hook(FAIL, hooks, args=[err])
790-
print_error(err.msg, exit_on_error=True, exit_code=1)
790+
print_error(err.msg, exit_on_error=True, exit_code=err.exit_code)
791791
except KeyboardInterrupt as err:
792792
run_hook(CANCEL, hooks, args=[err])
793793
print_error("Cancelled by user: %s" % err)

easybuild/tools/build_log.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040
import tempfile
4141
from copy import copy
4242
from datetime import datetime
43+
from enum import IntEnum
4344

4445
from easybuild.base import fancylogger
4546
from easybuild.base.exceptions import LoggedException
4647
from easybuild.tools.version import VERSION, this_is_easybuild
4748

48-
4949
# EasyBuild message prefix
5050
EB_MSG_PREFIX = "=="
5151

@@ -71,6 +71,55 @@
7171
logging.addLevelName(DEVEL_LOG_LEVEL, 'DEVEL')
7272

7373

74+
class EasyBuildExit(IntEnum):
75+
"""
76+
Table of exit codes
77+
"""
78+
SUCCESS = 0
79+
ERROR = 1
80+
# core errors
81+
OPTION_ERROR = 2
82+
VALUE_ERROR = 3
83+
MISSING_EASYCONFIG = 4
84+
EASYCONFIG_ERROR = 5
85+
MISSING_EASYBLOCK = 6
86+
EASYBLOCK_ERROR = 7
87+
MODULE_ERROR = 8
88+
# step errors in order of execution
89+
FAIL_FETCH_STEP = 10
90+
FAIL_READY_STEP = 11
91+
FAIL_SOURCE_STEP = 12
92+
FAIL_PATCH_STEP = 13
93+
FAIL_PREPARE_STEP = 14
94+
FAIL_CONFIGURE_STEP = 15
95+
FAIL_BUILD_STEP = 16
96+
FAIL_TEST_STEP = 17
97+
FAIL_INSTALL_STEP = 18
98+
FAIL_EXTENSIONS_STEP = 19
99+
FAIL_POST_ITER_STEP = 20
100+
FAIL_POST_PROC_STEP = 21
101+
FAIL_SANITY_CHECK_STEP = 22
102+
FAIL_CLEANUP_STEP = 23
103+
FAIL_MODULE_STEP = 24
104+
FAIL_PERMISSIONS_STEP = 25
105+
FAIL_PACKAGE_STEP = 26
106+
FAIL_TEST_CASES_STEP = 27
107+
# errors on missing things
108+
MISSING_SOURCES = 30
109+
MISSING_DEPENDENCY = 31
110+
MISSING_SYSTEM_DEPENDENCY = 32
111+
MISSING_EB_DEPENDENCY = 33
112+
# errors on specific task failures
113+
FAIL_SYSTEM_CHECK = 40
114+
FAIL_DOWNLOAD = 41
115+
FAIL_CHECKSUM = 42
116+
FAIL_EXTRACT = 43
117+
FAIL_PATCH_APPLY = 44
118+
FAIL_SANITY_CHECK = 45
119+
FAIL_MODULE_WRITE = 46
120+
FAIL_GITHUB = 47
121+
122+
74123
class EasyBuildError(LoggedException):
75124
"""
76125
EasyBuildError is thrown when EasyBuild runs into something horribly wrong.
@@ -80,12 +129,13 @@ class EasyBuildError(LoggedException):
80129
# always include location where error was raised from, even under 'python -O'
81130
INCLUDE_LOCATION = True
82131

83-
def __init__(self, msg, *args):
132+
def __init__(self, msg, *args, exit_code=EasyBuildExit.ERROR, **kwargs):
84133
"""Constructor: initialise EasyBuildError instance."""
85134
if args:
86135
msg = msg % args
87-
LoggedException.__init__(self, msg)
136+
LoggedException.__init__(self, msg, exit_code=exit_code, **kwargs)
88137
self.msg = msg
138+
self.exit_code = exit_code
89139

90140
def __str__(self):
91141
"""Return string representation of this EasyBuildError instance."""

easybuild/tools/config.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from easybuild.base import fancylogger
5151
from easybuild.base.frozendict import FrozenDictKnownKeys
5252
from easybuild.base.wrapper import create_base_metaclass
53-
from easybuild.tools.build_log import EasyBuildError
53+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit
5454

5555
try:
5656
import rich # noqa
@@ -506,7 +506,10 @@ def get_items_check_required(self):
506506
"""
507507
missing = [x for x in self.KNOWN_KEYS if x not in self]
508508
if len(missing) > 0:
509-
raise EasyBuildError("Cannot determine value for configuration variables %s. Please specify it.", missing)
509+
raise EasyBuildError(
510+
"Cannot determine value for configuration variables %s. Please specify it.", ', '.join(missing),
511+
exit_code=EasyBuildExit.OPTION_ERROR
512+
)
510513

511514
return self.items()
512515

@@ -539,7 +542,10 @@ def init(options, config_options_dict):
539542
tmpdict['sourcepath'] = sourcepath.split(':')
540543
_log.debug("Converted source path ('%s') to a list of paths: %s" % (sourcepath, tmpdict['sourcepath']))
541544
elif not isinstance(sourcepath, (tuple, list)):
542-
raise EasyBuildError("Value for sourcepath has invalid type (%s): %s", type(sourcepath), sourcepath)
545+
raise EasyBuildError(
546+
"Value for sourcepath has invalid type (%s): %s", type(sourcepath), sourcepath,
547+
exit_code=EasyBuildExit.OPTION_ERROR
548+
)
543549

544550
# initialize configuration variables (any future calls to ConfigurationVariables() will yield the same instance
545551
variables = ConfigurationVariables(tmpdict, ignore_unknown_keys=True)
@@ -623,7 +629,7 @@ def build_option(key, **kwargs):
623629
error_msg = "Undefined build option: '%s'. " % key
624630
error_msg += "Make sure you have set up the EasyBuild configuration using set_up_configuration() "
625631
error_msg += "(from easybuild.tools.options) in case you're not using EasyBuild via the 'eb' CLI."
626-
raise EasyBuildError(error_msg)
632+
raise EasyBuildError(error_msg, exit_code=EasyBuildExit.OPTION_ERROR)
627633

628634

629635
def update_build_option(key, value):
@@ -688,7 +694,10 @@ def install_path(typ=None):
688694

689695
known_types = ['modules', 'software']
690696
if typ not in known_types:
691-
raise EasyBuildError("Unknown type specified in install_path(): %s (known: %s)", typ, ', '.join(known_types))
697+
raise EasyBuildError(
698+
"Unknown type specified in install_path(): %s (known: %s)", typ, ', '.join(known_types),
699+
exit_code=EasyBuildExit.OPTION_ERROR
700+
)
692701

693702
variables = ConfigurationVariables()
694703

@@ -780,7 +789,10 @@ def get_output_style():
780789
output_style = OUTPUT_STYLE_BASIC
781790

782791
if output_style == OUTPUT_STYLE_RICH and not HAVE_RICH:
783-
raise EasyBuildError("Can't use '%s' output style, Rich Python package is not available!", OUTPUT_STYLE_RICH)
792+
raise EasyBuildError(
793+
"Can't use '%s' output style, Rich Python package is not available!", OUTPUT_STYLE_RICH,
794+
exit_code=EasyBuildExit.MISSING_EB_DEPENDENCY
795+
)
784796

785797
return output_style
786798

@@ -805,8 +817,10 @@ def log_file_format(return_directory=False, ec=None, date=None, timestamp=None):
805817

806818
logfile_format = ConfigurationVariables()['logfile_format']
807819
if not isinstance(logfile_format, tuple) or len(logfile_format) != 2:
808-
raise EasyBuildError("Incorrect log file format specification, should be 2-tuple (<dir>, <filename>): %s",
809-
logfile_format)
820+
raise EasyBuildError(
821+
"Incorrect log file format specification, should be 2-tuple (<dir>, <filename>): %s", logfile_format,
822+
exit_code=EasyBuildExit.OPTION_ERROR
823+
)
810824

811825
idx = int(not return_directory)
812826
res = ConfigurationVariables()['logfile_format'][idx] % {
@@ -913,7 +927,10 @@ def find_last_log(curlog):
913927
sorted_paths = [p for (_, p) in sorted(paths)]
914928

915929
except OSError as err:
916-
raise EasyBuildError("Failed to locate/select/order log files matching '%s': %s", glob_pattern, err)
930+
raise EasyBuildError(
931+
"Failed to locate/select/order log files matching '%s': %s", glob_pattern, err,
932+
exit_code=EasyBuildExit.OPTION_ERROR
933+
)
917934

918935
try:
919936
# log of current session is typically listed last, should be taken into account

easybuild/tools/containers/apptainer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import os
3030
import re
3131

32-
from easybuild.tools.build_log import EasyBuildError, print_msg
32+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg
3333
from easybuild.tools.containers.singularity import SingularityContainer
3434
from easybuild.tools.config import CONT_IMAGE_FORMAT_EXT3, CONT_IMAGE_FORMAT_SANDBOX
3535
from easybuild.tools.config import CONT_IMAGE_FORMAT_SIF, CONT_IMAGE_FORMAT_SQUASHFS
@@ -49,7 +49,7 @@ def apptainer_version():
4949
"""Get Apptainer version."""
5050
version_cmd = "apptainer --version"
5151
res = run_shell_cmd(version_cmd, hidden=True, in_dry_run=True)
52-
if res.exit_code:
52+
if res.exit_code != EasyBuildExit.SUCCESS:
5353
raise EasyBuildError(f"Error running '{version_cmd}': {res.output}")
5454

5555
regex_res = re.search(r"\d+\.\d+(\.\d+)?", res.output.strip())

easybuild/tools/containers/singularity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import re
3535

3636
from easybuild.tools import LooseVersion
37-
from easybuild.tools.build_log import EasyBuildError, print_msg
37+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg
3838
from easybuild.tools.config import CONT_IMAGE_FORMAT_EXT3, CONT_IMAGE_FORMAT_SANDBOX
3939
from easybuild.tools.config import CONT_IMAGE_FORMAT_SIF, CONT_IMAGE_FORMAT_SQUASHFS
4040
from easybuild.tools.config import build_option, container_path
@@ -163,7 +163,7 @@ def singularity_version():
163163
"""Get Singularity version."""
164164
version_cmd = "singularity --version"
165165
res = run_shell_cmd(version_cmd, hidden=True, in_dry_run=True)
166-
if res.exit_code:
166+
if res.exit_code != EasyBuildExit.SUCCESS:
167167
raise EasyBuildError(f"Error running '{version_cmd}': {res.output}")
168168

169169
regex_res = re.search(r"\d+\.\d+(\.\d+)?", res.output.strip())

easybuild/tools/containers/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from functools import reduce
3535

3636
from easybuild.tools import LooseVersion
37-
from easybuild.tools.build_log import EasyBuildError, print_msg
37+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg
3838
from easybuild.tools.filetools import which
3939
from easybuild.tools.run import run_shell_cmd
4040

@@ -77,7 +77,7 @@ def check_tool(tool_name, min_tool_version=None):
7777

7878
version_cmd = f"{tool_name} --version"
7979
res = run_shell_cmd(version_cmd, hidden=True, in_dry_run=True)
80-
if res.exit_code:
80+
if res.exit_code != EasyBuildExit.SUCCESS:
8181
raise EasyBuildError(f"Error running '{version_cmd}' for tool {tool_name} with output: {res.output}")
8282

8383
regex_res = re.search(r"\d+\.\d+(\.\d+)?", res.output.strip())

0 commit comments

Comments
 (0)