From 05b3873a3fc4e4701e3bf36493687701bb668a14 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 10 Aug 2022 19:27:37 +0200 Subject: [PATCH 01/53] Allow reconfiguring. Then, in main, loop over items in the EasyStack file, each time reconfiguring EasyBuild with EasyConfig-specific options that might be listed in the EasyStack file --- easybuild/main.py | 338 +++++++++++++++++++++---------------- easybuild/tools/options.py | 11 +- 2 files changed, 205 insertions(+), 144 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 5f052cf701..0d82167561 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -212,152 +212,14 @@ def clean_exit(logfile, tmpdir, testing, silent=False): cleanup(logfile, tmpdir, testing, silent=silent) sys.exit(0) + +def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks): -def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): - """ - Main function: parse command line options, and act accordingly. - :param args: command line arguments to use - :param logfile: log file to use - :param do_build: whether or not to actually perform the build - :param testing: enable testing mode - """ - - register_lock_cleanup_signal_handlers() - - # if $CDPATH is set, unset it, it'll only cause trouble... - # see https://github.com/easybuilders/easybuild-framework/issues/2944 - if 'CDPATH' in os.environ: - del os.environ['CDPATH'] - - # purposely session state very early, to avoid modules loaded by EasyBuild meddling in - init_session_state = session_state() - - eb_go, cfg_settings = set_up_configuration(args=args, logfile=logfile, testing=testing) - options, orig_paths = eb_go.options, eb_go.args - - global _log + # Unpack cfg_settings (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, - from_pr_list, tweaked_ecs_paths) = cfg_settings - - # load hook implementations (if any) - hooks = load_hooks(options.hooks) - - run_hook(START, hooks) + from_pr_list, tweaked_ecs_paths) = cfg_settings - if modtool is None: - modtool = modules_tool(testing=testing) - - # check whether any (EasyBuild-generated) modules are loaded already in the current session - modtool.check_loaded_modules() - - if options.last_log: - # print location to last log file, and exit - last_log = find_last_log(logfile) or '(none)' - print_msg(last_log, log=_log, prefix=False) - - # if easystack is provided with the command, commands with arguments from it will be executed - if options.easystack: - # TODO add general_options (i.e. robot) to build options - orig_paths, general_options = parse_easystack(options.easystack) - if general_options: - print_warning("Specifying options in easystack files is not supported yet. They are parsed, but ignored.") - - # check whether packaging is supported when it's being used - if options.package: - check_pkg_support() - else: - _log.debug("Packaging not enabled, so not checking for packaging support.") - - # search for easyconfigs, if a query is specified - if search_query: - search_easyconfigs(search_query, short=options.search_short, filename_only=options.search_filename, - terse=options.terse) - - if options.check_eb_deps: - print_checks(check_easybuild_deps(modtool)) - - # GitHub options that warrant a silent cleanup & exit - if options.check_github: - check_github() - - elif options.install_github_token: - install_github_token(options.github_user, silent=build_option('silent')) - - elif options.close_pr: - close_pr(options.close_pr, motivation_msg=options.close_pr_msg) - - elif options.list_prs: - print(list_prs(options.list_prs)) - - elif options.merge_pr: - merge_pr(options.merge_pr) - - elif options.review_pr: - print(review_pr(pr=options.review_pr, colored=use_color(options.color), testing=testing, - max_ecs=options.review_pr_max, filter_ecs=options.review_pr_filter)) - - elif options.add_pr_labels: - add_pr_labels(options.add_pr_labels) - - elif options.list_installed_software: - detailed = options.list_installed_software == 'detailed' - print(list_software(output_format=options.output_format, detailed=detailed, only_installed=True)) - - elif options.list_software: - print(list_software(output_format=options.output_format, detailed=options.list_software == 'detailed')) - - elif options.create_index: - print_msg("Creating index for %s..." % options.create_index, prefix=False) - index_fp = dump_index(options.create_index, max_age_sec=options.index_max_age) - index = load_index(options.create_index) - print_msg("Index created at %s (%d files)" % (index_fp, len(index)), prefix=False) - - # non-verbose cleanup after handling GitHub integration stuff or printing terse info - early_stop_options = [ - options.add_pr_labels, - options.check_eb_deps, - options.check_github, - options.create_index, - options.install_github_token, - options.list_installed_software, - options.list_software, - options.close_pr, - options.list_prs, - options.merge_pr, - options.review_pr, - options.terse, - search_query, - ] - if any(early_stop_options): - clean_exit(logfile, eb_tmpdir, testing, silent=True) - - # update session state - eb_config = eb_go.generate_cmd_line(add_default=True) - modlist = modtool.list() # build options must be initialized first before 'module list' works - init_session_state.update({'easybuild_configuration': eb_config}) - init_session_state.update({'module_list': modlist}) - _log.debug("Initial session state: %s" % init_session_state) - - if options.skip_test_step: - if options.ignore_test_failure: - raise EasyBuildError("Found both ignore-test-failure and skip-test-step enabled. " - "Please use only one of them.") - else: - print_warning("Will not run the test step as requested via skip-test-step. " - "Consider using ignore-test-failure instead and verify the results afterwards") - - # determine easybuild-easyconfigs package install path - easyconfigs_pkg_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR) - if not easyconfigs_pkg_paths: - _log.warning("Failed to determine install path for easybuild-easyconfigs package.") - - if options.install_latest_eb_release: - if orig_paths: - raise EasyBuildError("Installing the latest EasyBuild release can not be combined with installing " - "other easyconfigs") - else: - eb_file = find_easybuild_easyconfig() - orig_paths.append(eb_file) + # Reload hooks, they may have changed in case of EasyConfig specific options in an EasyStack file if options.copy_ec: # figure out list of files to copy + target location (taking into account --from-pr) @@ -585,6 +447,196 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): run_hook(END, hooks) + return overall_success + + +def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): + """ + Main function: parse command line options, and act accordingly. + :param args: command line arguments to use + :param logfile: log file to use + :param do_build: whether or not to actually perform the build + :param testing: enable testing mode + """ + + register_lock_cleanup_signal_handlers() + + # if $CDPATH is set, unset it, it'll only cause trouble... + # see https://github.com/easybuilders/easybuild-framework/issues/2944 + if 'CDPATH' in os.environ: + del os.environ['CDPATH'] + + # purposely session state very early, to avoid modules loaded by EasyBuild meddling in + init_session_state = session_state() + print(f"first time: args={args}") + eb_go, cfg_settings = set_up_configuration(args=args, logfile=logfile, testing=testing) + options, orig_paths = eb_go.options, eb_go.args + print(f"eb_go.args: {eb_go.args}") + print(f"options: {options}") + + global _log + (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, + from_pr_list, tweaked_ecs_paths) = cfg_settings + + # load hook implementations (if any) + hooks = load_hooks(options.hooks) + + run_hook(START, hooks) + + if modtool is None: + modtool = modules_tool(testing=testing) + + # check whether any (EasyBuild-generated) modules are loaded already in the current session + modtool.check_loaded_modules() + + if options.last_log: + # print location to last log file, and exit + last_log = find_last_log(logfile) or '(none)' + print_msg(last_log, log=_log, prefix=False) + + # if easystack is provided with the command, commands with arguments from it will be executed + if options.easystack: + # TODO add general_options (i.e. robot) to build options + orig_paths, opts_per_ec = parse_easystack(options.easystack) + if opts_per_ec: + print_warning("Specifying options in easystack files is not supported yet. They are parsed, but ignored.") + + # check whether packaging is supported when it's being used + if options.package: + check_pkg_support() + else: + _log.debug("Packaging not enabled, so not checking for packaging support.") + + # search for easyconfigs, if a query is specified + if search_query: + search_easyconfigs(search_query, short=options.search_short, filename_only=options.search_filename, + terse=options.terse) + + if options.check_eb_deps: + print_checks(check_easybuild_deps(modtool)) + + # GitHub options that warrant a silent cleanup & exit + if options.check_github: + check_github() + + elif options.install_github_token: + install_github_token(options.github_user, silent=build_option('silent')) + + elif options.close_pr: + close_pr(options.close_pr, motivation_msg=options.close_pr_msg) + + elif options.list_prs: + print(list_prs(options.list_prs)) + + elif options.merge_pr: + merge_pr(options.merge_pr) + + elif options.review_pr: + print(review_pr(pr=options.review_pr, colored=use_color(options.color), testing=testing, + max_ecs=options.review_pr_max, filter_ecs=options.review_pr_filter)) + + elif options.add_pr_labels: + add_pr_labels(options.add_pr_labels) + + elif options.list_installed_software: + detailed = options.list_installed_software == 'detailed' + print(list_software(output_format=options.output_format, detailed=detailed, only_installed=True)) + + elif options.list_software: + print(list_software(output_format=options.output_format, detailed=options.list_software == 'detailed')) + + elif options.create_index: + print_msg("Creating index for %s..." % options.create_index, prefix=False) + index_fp = dump_index(options.create_index, max_age_sec=options.index_max_age) + index = load_index(options.create_index) + print_msg("Index created at %s (%d files)" % (index_fp, len(index)), prefix=False) + + # non-verbose cleanup after handling GitHub integration stuff or printing terse info + early_stop_options = [ + options.add_pr_labels, + options.check_eb_deps, + options.check_github, + options.create_index, + options.install_github_token, + options.list_installed_software, + options.list_software, + options.close_pr, + options.list_prs, + options.merge_pr, + options.review_pr, + options.terse, + search_query, + ] + if any(early_stop_options): + clean_exit(logfile, eb_tmpdir, testing, silent=True) + + # update session state + eb_config = eb_go.generate_cmd_line(add_default=True) + modlist = modtool.list() # build options must be initialized first before 'module list' works + init_session_state.update({'easybuild_configuration': eb_config}) + init_session_state.update({'module_list': modlist}) + _log.debug("Initial session state: %s" % init_session_state) + + if options.skip_test_step: + if options.ignore_test_failure: + raise EasyBuildError("Found both ignore-test-failure and skip-test-step enabled. " + "Please use only one of them.") + else: + print_warning("Will not run the test step as requested via skip-test-step. " + "Consider using ignore-test-failure instead and verify the results afterwards") + + # determine easybuild-easyconfigs package install path + easyconfigs_pkg_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR) + if not easyconfigs_pkg_paths: + _log.warning("Failed to determine install path for easybuild-easyconfigs package.") + + if options.install_latest_eb_release: + if orig_paths: + raise EasyBuildError("Installing the latest EasyBuild release can not be combined with installing " + "other easyconfigs") + else: + eb_file = find_easybuild_easyconfig() + orig_paths.append(eb_file) + + if options.easystack: + # TODO: insert fast loop that validates if all command line options are valid + # for path in orig_paths: + # validate_command_opts(args, opts_per_ec[path]) + print(f"Looping over {orig_paths}") + # Loop over each item in the EasyStack file, each time updating the config + # This is because each item in an EasyStack file can have options associated with it + for path in orig_paths: + print(f"Running on {path} from EasyStack file") + # NOTE: not sure if this is needed. Is the EasyConfigs cache preserved throughout loops of this iteration? + # Current 'path' may have different options associated with it. Thus, resolution of EasyConfigs + # to full paths should not be read from cache, but redetermined. Thus, we wipe the cache + # wipe_cache() # Wipes the easyconfig cache with _easyconfigs_cache.clear() + + # EasyBuild configuration is a singleton, since it should _normally_ only be set up once + # For EasyStack files, it needs to be set up once _per entry_ in the EasyStack file + # Thus, we need to wipe the singleton + # wipe_build_options_singleton() # Should do BuildOptions.__class__.instances.clear() + + # Determine new arguments by merging the command line options with the easyconfig-specific options + # from the EasyStack file. Some commands may be overwritten (easyconfig-specific wins), + # some may be appended (e.g. --from-pr) + # new_args = merge_command_line_opts(args, opts_per_ec[path]) + # set_up_configuration(new_args) + # NOTE: for now just call with original arguments to see if we can get this new logic to work + + # HARDCODED FOR TESTING + args = sys.argv[1:] +# print(f"args: {args}") + args.extend(['--filter-deps', 'Bison', '--hidden']) +# print(f"New args: {args}") + eb_go, cfg_settings = set_up_configuration(args=args, logfile=logfile, testing=testing, reconfigure=True) +# print(f"eb_go.options: {eb_go.options}") + + hooks = load_hooks(options.hooks) + overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, hooks) + else: + overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks) + # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if overall_success: diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 6128c3e650..28554e28c3 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -72,6 +72,7 @@ from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS from easybuild.tools.config import OUTPUT_STYLE_AUTO, OUTPUT_STYLES, WARN from easybuild.tools.config import get_pretend_installpath, init, init_build_options, mk_full_default_path +from easybuild.tools.config import BuildOptions from easybuild.tools.configobj import ConfigObj, ConfigObjError from easybuild.tools.docs import FORMAT_TXT, FORMAT_RST from easybuild.tools.docs import avail_cfgfile_constants, avail_easyconfig_constants, avail_easyconfig_licenses @@ -1496,7 +1497,7 @@ def check_root_usage(allow_use_as_root=False): "so let's end this here.") -def set_up_configuration(args=None, logfile=None, testing=False, silent=False): +def set_up_configuration(args=None, logfile=None, testing=False, silent=False, reconfigure=False): """ Set up EasyBuild configuration, by parsing configuration settings & initialising build options. @@ -1504,6 +1505,7 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False): :param logfile: log file to use :param testing: enable testing mode :param silent: stay silent (no printing) + :param reconfigure: reconfigure singletons that hold configuration dictionaries. Use with care: normally, configuration shouldn't be changed during a run. Exceptions are when looping over items in EasyStack files """ # set up fake 'vsc' Python package, to catch easyblocks/scripts that still import from vsc.* namespace @@ -1581,6 +1583,13 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False): 'try_to_generate': try_to_generate, 'valid_stops': [x[0] for x in EasyBlock.get_steps()], } + + # Remove existing singletons if reconfigure==True (allows reconfiguration when looping over EasyStack items) + if reconfigure: + BuildOptions.__class__._instances.clear() + elif len(BuildOptions.__class__._instances) > 0: + log.warn("set_up_configuration is about to call init() and init_build_options(). However, the singletons that these functions normally initialize already exist. If configuration should be changed, this may lead to unexpected behavior, as the existing singletons will be returned. If you intended to reconfigure, you should probably pass reconfigure=True to set_up_configuration().") + # initialise the EasyBuild configuration & build options init(options, config_options_dict) init_build_options(build_options=build_options, cmdline_options=options) From bbd3630482624e965c308fd097ab8b494f6cd7bd Mon Sep 17 00:00:00 2001 From: casparl Date: Thu, 15 Sep 2022 18:36:07 +0200 Subject: [PATCH 02/53] Added support for merging command line arguments and easyconfig specific arguments from the EasyStack file. This commit should contain a functional implementation of the option support for EasyStack files. Now, we only need to clean it up... --- easybuild/main.py | 88 +++++++++++++++++++++--------- easybuild/tools/options.py | 108 +++++++++++++++++++++++++++++++++---- 2 files changed, 161 insertions(+), 35 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 0d82167561..a3a8ba2c68 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -66,7 +66,7 @@ from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr from easybuild.tools.hooks import START, END, load_hooks, run_hook from easybuild.tools.modules import modules_tool -from easybuild.tools.options import set_up_configuration, use_color +from easybuild.tools.options import set_up_configuration, use_color, merge_command_line_opts, dict_to_argslist from easybuild.tools.output import COLOR_GREEN, COLOR_RED, STATUS_BAR, colorize, print_checks, rich_live_cm from easybuild.tools.output import start_progress_bar, stop_progress_bar, update_progress_bar from easybuild.tools.robot import check_conflicts, dry_run, missing_deps, resolve_dependencies, search_easyconfigs @@ -213,14 +213,24 @@ def clean_exit(logfile, tmpdir, testing, silent=False): sys.exit(0) -def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks): +def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, skip_clean_exit=False): + """ + Remainder of the main function, after orig_paths has been determined + + :param orig_paths: list of EasyConfig names + :param options: eb_go.options, as returned by set_up_configuration + :param cfg_settings: as returned by set_up_configuration + :param modtool: the modules tool, as returned by modules_tool() + :param testing: bool whether we're running in test mode + :param init_session_state: initial session state, to use in test reports + :param hooks: hooks, as loaded by load_hooks from the options + :param skip_clean_exit: normally, rest_of_main calls sys.exit(0) whenever certain runs (e.g. a dry-run) have completed. When rest_of_main is called as part of a loop, you don't want to exit, but want to return instead. This boolean controls that behaviour. + """ # Unpack cfg_settings (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, from_pr_list, tweaked_ecs_paths) = cfg_settings - # Reload hooks, they may have changed in case of EasyConfig specific options in an EasyStack file - if options.copy_ec: # figure out list of files to copy + target location (taking into account --from-pr) orig_paths, target_path = det_copy_ec_specs(orig_paths, from_pr_list) @@ -256,7 +266,10 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi print_msg("Contents of %s:" % path) print_msg(read_file(path), prefix=False) - clean_exit(logfile, eb_tmpdir, testing) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing) + else: + return if determined_paths: # transform paths into tuples, use 'False' to indicate the corresponding easyconfig files were not generated @@ -286,7 +299,10 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi # handle --check-contrib & --check-style options if run_contrib_style_checks([ec['ec'] for ec in easyconfigs], options.check_contrib, options.check_style): - clean_exit(logfile, eb_tmpdir, testing) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing) + else: + return # verify easyconfig filenames, if desired if options.verify_easyconfig_filenames: @@ -303,7 +319,10 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi if options.containerize: # if --containerize/-C create a container recipe (and optionally container image), and stop containerize(easyconfigs) - clean_exit(logfile, eb_tmpdir, testing) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing) + else: + return forced = options.force or options.rebuild dry_run_mode = options.dry_run or options.dry_run_short or options.missing_modules @@ -349,7 +368,10 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi clean_up_easyconfigs(tweaked_ecs_in_all_ecs) copy_files(tweaked_ecs_in_all_ecs, target_path, allow_empty=False, verbose=True) - clean_exit(logfile, eb_tmpdir, testing) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing) + else: + return # creating/updating PRs if pr_options: @@ -398,20 +420,29 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi # cleanup and exit after dry run, searching easyconfigs or submitting regression test stop_options = [options.check_conflicts, dry_run_mode, options.dump_env_script, options.inject_checksums] if any(no_ec_opts) or any(stop_options): - clean_exit(logfile, eb_tmpdir, testing) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing) + else: + return # create dependency graph and exit if options.dep_graph: _log.info("Creating dependency graph %s" % options.dep_graph) dep_graph(options.dep_graph, ordered_ecs) - clean_exit(logfile, eb_tmpdir, testing, silent=True) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing, silent=True) + else: + return # submit build as job(s), clean up and exit if options.job: submit_jobs(ordered_ecs, eb_go.generate_cmd_line(), testing=testing) if not testing: print_msg("Submitted parallel build jobs, exiting now") - clean_exit(logfile, eb_tmpdir, testing) + if not skip_clean_exit: + clean_exit(logfile, eb_tmpdir, testing) + else: + return # build software, will exit when errors occurs (except when testing) if not testing or (testing and do_build): @@ -603,6 +634,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # for path in orig_paths: # validate_command_opts(args, opts_per_ec[path]) print(f"Looping over {orig_paths}") + print(f"With opts_per_ec: {opts_per_ec}") # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it for path in orig_paths: @@ -610,30 +642,36 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # NOTE: not sure if this is needed. Is the EasyConfigs cache preserved throughout loops of this iteration? # Current 'path' may have different options associated with it. Thus, resolution of EasyConfigs # to full paths should not be read from cache, but redetermined. Thus, we wipe the cache - # wipe_cache() # Wipes the easyconfig cache with _easyconfigs_cache.clear() + # wipe_cache() # Wipes the easyconfig cache with _easyconfigs_cache.clear() # TODO: implement # EasyBuild configuration is a singleton, since it should _normally_ only be set up once # For EasyStack files, it needs to be set up once _per entry_ in the EasyStack file # Thus, we need to wipe the singleton - # wipe_build_options_singleton() # Should do BuildOptions.__class__.instances.clear() + # wipe_build_options_singleton() # Should do BuildOptions.__class__.instances.clear() # TODO: impelement # Determine new arguments by merging the command line options with the easyconfig-specific options # from the EasyStack file. Some commands may be overwritten (easyconfig-specific wins), # some may be appended (e.g. --from-pr) - # new_args = merge_command_line_opts(args, opts_per_ec[path]) - # set_up_configuration(new_args) - # NOTE: for now just call with original arguments to see if we can get this new logic to work - - # HARDCODED FOR TESTING - args = sys.argv[1:] -# print(f"args: {args}") - args.extend(['--filter-deps', 'Bison', '--hidden']) -# print(f"New args: {args}") - eb_go, cfg_settings = set_up_configuration(args=args, logfile=logfile, testing=testing, reconfigure=True) -# print(f"eb_go.options: {eb_go.options}") + # TODO: probably, we shouldn't try to merge argument lists, as this is difficult. We should probably merge two EasyBuildOptions objects instead, as those are fully defined. That way, it's much easier to determine what 'merging' should actually do, as we don't have to first match arguments based on string matching. + if path in opts_per_ec: + print(f"Calling merge_command_line_opts with args: {args} and new_args: {opts_per_ec}") + if args is None: + args = sys.argv[1:] + ec_args = dict_to_argslist(opts_per_ec[path]) + # By appending ec_args to args, ec_args take priority + # We could create an additional EB option to give the user control over which of the two gets priority + new_args = args + ec_args + # new_args = ec_args + args + else: + # If no EasyConfig specific arguments are defined, sse original args. + # That way,set_up_configuration restores the original config + new_args = args + print(f"Calling set_up_configuration with args: {new_args}") + eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, reconfigure=True) + print(f"eb_go.options.from_pr: {eb_go.options.from_pr}") hooks = load_hooks(options.hooks) - overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, hooks) + overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, hooks, skip_clean_exit=True) else: overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 28554e28c3..cf1cd22050 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1445,16 +1445,7 @@ def det_location(opt, prefix=''): pretty_print_opts(opts_dict) -def parse_options(args=None, with_include=True): - """wrapper function for option parsing""" - if os.environ.get('DEBUG_EASYBUILD_OPTIONS', '0').lower() in ('1', 'true', 'yes', 'y'): - # very early debug, to debug the generaloption itself - fancylogger.logToScreen(enable=True) - fancylogger.setLogLevel('DEBUG') - - if args is None: - args = sys.argv[1:] - +def unroll_arguments(args): # unroll arguments that correspond to a combo of single-letter options # this is done to avoid interpreting -rD like "--robot D" instead of "--robot -D" eb_args = [] @@ -1466,6 +1457,21 @@ def parse_options(args=None, with_include=True): else: eb_args.append(arg) + return eb_args + + +def parse_options(args=None, with_include=True): + """wrapper function for option parsing""" + if os.environ.get('DEBUG_EASYBUILD_OPTIONS', '0').lower() in ('1', 'true', 'yes', 'y'): + # very early debug, to debug the generaloption itself + fancylogger.logToScreen(enable=True) + fancylogger.setLogLevel('DEBUG') + + if args is None: + args = sys.argv[1:] + + eb_args = unroll_arguments(args) + usage = "%prog [options] easyconfig [...]" description = ("Builds software based on easyconfig (or parse a directory).\n" "Provide one or more easyconfigs or directories, use -H or --help more information.") @@ -1887,3 +1893,85 @@ def set_tmpdir(tmpdir=None, raise_error=False): raise EasyBuildError("Failed to test whether temporary directory allows to execute files: %s", err) return current_tmpdir + + +def dict_to_argslist(args_dict): + """ + Convert a dictionary with key-value arguments to a list similar to sys.argv[1:]. + Can by used to convert e.g. EasyConfig specific options from an EasyStack file to an argument list + + :param args_dict: dictionary with options (keys) and parameters (values), e.g. {'from_pr': [1234], 'robot': True} + :return: a list of arguments, similar to what sys.argv[1:] would return + """ + + print(f"args_dict: {args_dict}") + args = [] + for arg in args_dict: + if len(arg) == 1: + prefix = '-' + else: + prefix = '--' + option = prefix + arg + value = args_dict[arg] + args.append(option) + if value is not None: + args.append(value) + + print(f"Constructed args list: {args}") + return args + +def argslist_to_dict(args): + """ + Convert a list of arguments (similar to sys.argv[1:]) to a dictionary where the argument names are keys, + and the argument values are dictionary values. + + :param args: list of arguments, e.g. '--from-pr 1234' + :return: a dictionary of arguments, e.g. {from-pr: 1234} + """ + # Should we implement this? Or continue down the route where we make an EasyBuildOptions object for the global arguments, one for the global arguments, and one for the EasyConfig-specific args in the EasyStack file? + # The problem with the second appraoch is that you can check for differences, but you don't know where the difference comes from: is it different because e.g. a command line arg overwrote some default? Because in that case, you probably want to _keep_ what the command line arg did (e.g. a -D on the command line with an EasyStack file should probably apply -D on _all_ easyconfigs in the EasyStack file...) + + +# TODO: probably, we should not try to merge args, it's a mess +# This issue is that it is difficult to merge e.g. +# ['--dry-run'] and ['--disable-dry-run'] +# or +# ['--from-pr', '4567'] and ['--from-pr', '1234'] +# The first would have to recognize that --dry-run and --disable-dry-run are essentially 'the same option' +# The second one would have to overwrite or append the value, if the option is find in args_orig. +# Similary, a +# ['-r'] +# is difficult since t has no parameter value. +# In short: it's tricky. Porbably, it's much easier to compare two EasyBuildOption objects, and overwrite behaviour here. These two objects fully specify _all_ option values, meaning there is _always_ something to overwrite, and the disable / enable options will just have parameter values true/false. +def merge_command_line_opts(args_orig, args_new): + """ + Merge two sets of command line options. For now, all command line options for args_new take priority. + In the future, we'll likely merge specific arguments like --from-pr differently, as these can be merged + simply by extending the list of arguments. + E.g. --from-pr=1234 in args_orig and --from-pr=5678 in args_new can be merged to --from-pr=1234,5678 + + :param args_orig: original set of command line arguments + :param args_new: new set of command line arguments, to be merged with the original set + :return: a single, valid, args object + """ + + print(f"merge_command_line_opts with args_orig: {args_orig}") + print(f"merge_command_line_opts with args_new: {args_new}") + args_orig = unroll_arguments(args_orig) + args_new = unroll_arguments(args_new) + + for arg in args_new: + print(f"Argument: {arg}") + + return args_new + + +def arg_is_option(arg): + """ + Check if arg starts with a dash. Can be used to check if an argument is an option, or a parameter + """ + if len(arg) > 0 and arg[0] == '-': + print(f"arg: {arg} is an option") + return True + else: + return False From 88a501da5c8180df30d9215856fd20af1fbb579a Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 16 Sep 2022 15:07:59 +0200 Subject: [PATCH 03/53] cleaned up logging, removed unnecessary functions that aren't used in the end --- easybuild/main.py | 26 +++++++---------- easybuild/tools/options.py | 60 ++------------------------------------ 2 files changed, 13 insertions(+), 73 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index a3a8ba2c68..f26f1959c4 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -66,7 +66,7 @@ from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr from easybuild.tools.hooks import START, END, load_hooks, run_hook from easybuild.tools.modules import modules_tool -from easybuild.tools.options import set_up_configuration, use_color, merge_command_line_opts, dict_to_argslist +from easybuild.tools.options import set_up_configuration, use_color, dict_to_argslist from easybuild.tools.output import COLOR_GREEN, COLOR_RED, STATUS_BAR, colorize, print_checks, rich_live_cm from easybuild.tools.output import start_progress_bar, stop_progress_bar, update_progress_bar from easybuild.tools.robot import check_conflicts, dry_run, missing_deps, resolve_dependencies, search_easyconfigs @@ -499,11 +499,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # purposely session state very early, to avoid modules loaded by EasyBuild meddling in init_session_state = session_state() - print(f"first time: args={args}") eb_go, cfg_settings = set_up_configuration(args=args, logfile=logfile, testing=testing) options, orig_paths = eb_go.options, eb_go.args - print(f"eb_go.args: {eb_go.args}") - print(f"options: {options}") global _log (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, @@ -527,10 +524,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # if easystack is provided with the command, commands with arguments from it will be executed if options.easystack: - # TODO add general_options (i.e. robot) to build options orig_paths, opts_per_ec = parse_easystack(options.easystack) - if opts_per_ec: - print_warning("Specifying options in easystack files is not supported yet. They are parsed, but ignored.") # check whether packaging is supported when it's being used if options.package: @@ -630,15 +624,14 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): orig_paths.append(eb_file) if options.easystack: - # TODO: insert fast loop that validates if all command line options are valid + _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) + # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, we want to know early on, and this loop potentially builds a lot of packages and could take very long # for path in orig_paths: # validate_command_opts(args, opts_per_ec[path]) - print(f"Looping over {orig_paths}") - print(f"With opts_per_ec: {opts_per_ec}") # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it for path in orig_paths: - print(f"Running on {path} from EasyStack file") + _log.debug("Starting build for %s" % path) # NOTE: not sure if this is needed. Is the EasyConfigs cache preserved throughout loops of this iteration? # Current 'path' may have different options associated with it. Thus, resolution of EasyConfigs # to full paths should not be read from cache, but redetermined. Thus, we wipe the cache @@ -654,7 +647,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # some may be appended (e.g. --from-pr) # TODO: probably, we shouldn't try to merge argument lists, as this is difficult. We should probably merge two EasyBuildOptions objects instead, as those are fully defined. That way, it's much easier to determine what 'merging' should actually do, as we don't have to first match arguments based on string matching. if path in opts_per_ec: - print(f"Calling merge_command_line_opts with args: {args} and new_args: {opts_per_ec}") + _log.debug("EasyConfig specific options have been specified for %s in the EasyStack file: %s" % (path, opts_per_ec[path])) if args is None: args = sys.argv[1:] ec_args = dict_to_argslist(opts_per_ec[path]) @@ -662,22 +655,25 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # We could create an additional EB option to give the user control over which of the two gets priority new_args = args + ec_args # new_args = ec_args + args + _log.info("Argument list for %s after merging command line arguments with EasyConfig specific options from the EasyStack file: %s" % (path, new_args)) else: # If no EasyConfig specific arguments are defined, sse original args. # That way,set_up_configuration restores the original config new_args = args - print(f"Calling set_up_configuration with args: {new_args}") eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, reconfigure=True) - print(f"eb_go.options.from_pr: {eb_go.options.from_pr}") hooks = load_hooks(options.hooks) overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, hooks, skip_clean_exit=True) + + # Loop done. If overall_success is not false, cleanup + if overall_success or overall_success is None: + cleanup(logfile, eb_tmpdir, testing) else: overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks) - # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if overall_success: + print("Running cleanup") cleanup(logfile, eb_tmpdir, testing) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index cf1cd22050..d2062cccc5 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1904,7 +1904,7 @@ def dict_to_argslist(args_dict): :return: a list of arguments, similar to what sys.argv[1:] would return """ - print(f"args_dict: {args_dict}") + _log.debug("Converting dictionary %s to argument list" % args_dict) args = [] for arg in args_dict: if len(arg) == 1: @@ -1917,61 +1917,5 @@ def dict_to_argslist(args_dict): if value is not None: args.append(value) - print(f"Constructed args list: {args}") + _log.debug("Converted dictionary %s to argument list %s" % (args_dict, args)) return args - -def argslist_to_dict(args): - """ - Convert a list of arguments (similar to sys.argv[1:]) to a dictionary where the argument names are keys, - and the argument values are dictionary values. - - :param args: list of arguments, e.g. '--from-pr 1234' - :return: a dictionary of arguments, e.g. {from-pr: 1234} - """ - # Should we implement this? Or continue down the route where we make an EasyBuildOptions object for the global arguments, one for the global arguments, and one for the EasyConfig-specific args in the EasyStack file? - # The problem with the second appraoch is that you can check for differences, but you don't know where the difference comes from: is it different because e.g. a command line arg overwrote some default? Because in that case, you probably want to _keep_ what the command line arg did (e.g. a -D on the command line with an EasyStack file should probably apply -D on _all_ easyconfigs in the EasyStack file...) - - -# TODO: probably, we should not try to merge args, it's a mess -# This issue is that it is difficult to merge e.g. -# ['--dry-run'] and ['--disable-dry-run'] -# or -# ['--from-pr', '4567'] and ['--from-pr', '1234'] -# The first would have to recognize that --dry-run and --disable-dry-run are essentially 'the same option' -# The second one would have to overwrite or append the value, if the option is find in args_orig. -# Similary, a -# ['-r'] -# is difficult since t has no parameter value. -# In short: it's tricky. Porbably, it's much easier to compare two EasyBuildOption objects, and overwrite behaviour here. These two objects fully specify _all_ option values, meaning there is _always_ something to overwrite, and the disable / enable options will just have parameter values true/false. -def merge_command_line_opts(args_orig, args_new): - """ - Merge two sets of command line options. For now, all command line options for args_new take priority. - In the future, we'll likely merge specific arguments like --from-pr differently, as these can be merged - simply by extending the list of arguments. - E.g. --from-pr=1234 in args_orig and --from-pr=5678 in args_new can be merged to --from-pr=1234,5678 - - :param args_orig: original set of command line arguments - :param args_new: new set of command line arguments, to be merged with the original set - :return: a single, valid, args object - """ - - print(f"merge_command_line_opts with args_orig: {args_orig}") - print(f"merge_command_line_opts with args_new: {args_new}") - args_orig = unroll_arguments(args_orig) - args_new = unroll_arguments(args_new) - - for arg in args_new: - print(f"Argument: {arg}") - - return args_new - - -def arg_is_option(arg): - """ - Check if arg starts with a dash. Can be used to check if an argument is an option, or a parameter - """ - if len(arg) > 0 and arg[0] == '-': - print(f"arg: {arg} is an option") - return True - else: - return False From 48ce5b345e60427fcff2e4ad8733ad1ffed3e77e Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 16 Sep 2022 15:19:39 +0200 Subject: [PATCH 04/53] Fixing too long lines etc --- easybuild/main.py | 13 ++++++++----- easybuild/tools/options.py | 14 +++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index f26f1959c4..25ce361bc0 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -625,13 +625,14 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): if options.easystack: _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) - # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, we want to know early on, and this loop potentially builds a lot of packages and could take very long + # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, + # we want to know early on, and this loop potentially builds a lot of packages and could take very long # for path in orig_paths: - # validate_command_opts(args, opts_per_ec[path]) + # validate_command_opts(args, opts_per_ec[path]) # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it for path in orig_paths: - _log.debug("Starting build for %s" % path) + _log.debug("Starting build for %s" % path) # NOTE: not sure if this is needed. Is the EasyConfigs cache preserved throughout loops of this iteration? # Current 'path' may have different options associated with it. Thus, resolution of EasyConfigs # to full paths should not be read from cache, but redetermined. Thus, we wipe the cache @@ -647,7 +648,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # some may be appended (e.g. --from-pr) # TODO: probably, we shouldn't try to merge argument lists, as this is difficult. We should probably merge two EasyBuildOptions objects instead, as those are fully defined. That way, it's much easier to determine what 'merging' should actually do, as we don't have to first match arguments based on string matching. if path in opts_per_ec: - _log.debug("EasyConfig specific options have been specified for %s in the EasyStack file: %s" % (path, opts_per_ec[path])) + _log.debug("EasyConfig specific options have been specified for " + "%s in the EasyStack file: %s" % (path, opts_per_ec[path])) if args is None: args = sys.argv[1:] ec_args = dict_to_argslist(opts_per_ec[path]) @@ -655,7 +657,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # We could create an additional EB option to give the user control over which of the two gets priority new_args = args + ec_args # new_args = ec_args + args - _log.info("Argument list for %s after merging command line arguments with EasyConfig specific options from the EasyStack file: %s" % (path, new_args)) + _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " + "options from the EasyStack file: %s" % (path, new_args)) else: # If no EasyConfig specific arguments are defined, sse original args. # That way,set_up_configuration restores the original config diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index d2062cccc5..1ced561b7e 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1511,7 +1511,8 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r :param logfile: log file to use :param testing: enable testing mode :param silent: stay silent (no printing) - :param reconfigure: reconfigure singletons that hold configuration dictionaries. Use with care: normally, configuration shouldn't be changed during a run. Exceptions are when looping over items in EasyStack files + :param reconfigure: reconfigure singletons that hold configuration dictionaries. Use with care: normally, + configuration shouldn't be changed during a run. Exceptions are when looping over items in EasyStack files """ # set up fake 'vsc' Python package, to catch easyblocks/scripts that still import from vsc.* namespace @@ -1594,7 +1595,11 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r if reconfigure: BuildOptions.__class__._instances.clear() elif len(BuildOptions.__class__._instances) > 0: - log.warn("set_up_configuration is about to call init() and init_build_options(). However, the singletons that these functions normally initialize already exist. If configuration should be changed, this may lead to unexpected behavior, as the existing singletons will be returned. If you intended to reconfigure, you should probably pass reconfigure=True to set_up_configuration().") + log.warn("set_up_configuration is about to call init() and init_build_options(). " + "However, the singletons that these functions normally initialize already exist. " + "If configuration should be changed, this may lead to unexpected behavior, as the existing " + "singletons will be returned. If you intended to reconfigure, you should probably pass " + "reconfigure=True to set_up_configuration().") # initialise the EasyBuild configuration & build options init(options, config_options_dict) @@ -1899,7 +1904,6 @@ def dict_to_argslist(args_dict): """ Convert a dictionary with key-value arguments to a list similar to sys.argv[1:]. Can by used to convert e.g. EasyConfig specific options from an EasyStack file to an argument list - :param args_dict: dictionary with options (keys) and parameters (values), e.g. {'from_pr': [1234], 'robot': True} :return: a list of arguments, similar to what sys.argv[1:] would return """ @@ -1908,9 +1912,9 @@ def dict_to_argslist(args_dict): args = [] for arg in args_dict: if len(arg) == 1: - prefix = '-' + prefix = '-' else: - prefix = '--' + prefix = '--' option = prefix + arg value = args_dict[arg] args.append(option) From 245cee34bae51c2b34345cadd57547cbd2f7146b Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 16 Sep 2022 15:21:24 +0200 Subject: [PATCH 05/53] Trailing whitespace removed --- easybuild/tools/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 1ced561b7e..6f61bd9db1 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1511,7 +1511,7 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r :param logfile: log file to use :param testing: enable testing mode :param silent: stay silent (no printing) - :param reconfigure: reconfigure singletons that hold configuration dictionaries. Use with care: normally, + :param reconfigure: reconfigure singletons that hold configuration dictionaries. Use with care: normally, configuration shouldn't be changed during a run. Exceptions are when looping over items in EasyStack files """ From b4c4b8aadd89b5f6dac335db51cd415ecd02f731 Mon Sep 17 00:00:00 2001 From: casparl Date: Mon, 19 Sep 2022 18:11:04 +0200 Subject: [PATCH 06/53] More cleanup of too long lines, added additional arg to rest_of_main. Probably need to pass more args there... --- easybuild/main.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 25ce361bc0..2a1021f45a 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -212,8 +212,9 @@ def clean_exit(logfile, tmpdir, testing, silent=False): cleanup(logfile, tmpdir, testing, silent=silent) sys.exit(0) - -def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, skip_clean_exit=False): + +def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build, + skip_clean_exit=False): """ Remainder of the main function, after orig_paths has been determined @@ -224,8 +225,10 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi :param testing: bool whether we're running in test mode :param init_session_state: initial session state, to use in test reports :param hooks: hooks, as loaded by load_hooks from the options - :param skip_clean_exit: normally, rest_of_main calls sys.exit(0) whenever certain runs (e.g. a dry-run) have completed. When rest_of_main is called as part of a loop, you don't want to exit, but want to return instead. This boolean controls that behaviour. - + :param do_build: whether or not to actually perform the build + :param skip_clean_exit: normally, rest_of_main calls sys.exit(0) whenever certain runs (e.g. a dry-run) have + completed. When rest_of_main is called as part of a loop, you don't want to exit, but want to return instead. + This boolean controls that behaviour. """ # Unpack cfg_settings (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, @@ -643,10 +646,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # Thus, we need to wipe the singleton # wipe_build_options_singleton() # Should do BuildOptions.__class__.instances.clear() # TODO: impelement - # Determine new arguments by merging the command line options with the easyconfig-specific options - # from the EasyStack file. Some commands may be overwritten (easyconfig-specific wins), - # some may be appended (e.g. --from-pr) - # TODO: probably, we shouldn't try to merge argument lists, as this is difficult. We should probably merge two EasyBuildOptions objects instead, as those are fully defined. That way, it's much easier to determine what 'merging' should actually do, as we don't have to first match arguments based on string matching. + # If EasyConfig specific arguments were supplied in EasyStack file + # merge arguments with original command line args if path in opts_per_ec: _log.debug("EasyConfig specific options have been specified for " "%s in the EasyStack file: %s" % (path, opts_per_ec[path])) @@ -654,9 +655,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): args = sys.argv[1:] ec_args = dict_to_argslist(opts_per_ec[path]) # By appending ec_args to args, ec_args take priority - # We could create an additional EB option to give the user control over which of the two gets priority new_args = args + ec_args - # new_args = ec_args + args _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " "options from the EasyStack file: %s" % (path, new_args)) else: @@ -666,13 +665,14 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, reconfigure=True) hooks = load_hooks(options.hooks) - overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, hooks, skip_clean_exit=True) + overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, + hooks, do_build, skip_clean_exit=True) # Loop done. If overall_success is not false, cleanup if overall_success or overall_success is None: cleanup(logfile, eb_tmpdir, testing) else: - overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks) + overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if overall_success: From 5b908a46f84f35527960e346a9d66c874852dfd0 Mon Sep 17 00:00:00 2001 From: casparl Date: Mon, 19 Sep 2022 18:12:45 +0200 Subject: [PATCH 07/53] Shortened line --- easybuild/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easybuild/main.py b/easybuild/main.py index 2a1021f45a..57001b5b3b 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -672,7 +672,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): if overall_success or overall_success is None: cleanup(logfile, eb_tmpdir, testing) else: - overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build) + overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, + do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if overall_success: From 32640684a2add84b11db4a02e60e68f334f88104 Mon Sep 17 00:00:00 2001 From: casparl Date: Mon, 19 Sep 2022 18:15:01 +0200 Subject: [PATCH 08/53] Remove the todo to clean the singleton, this is done now in set_up_configuration when reconfigure=True --- easybuild/main.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 57001b5b3b..b6a5ead860 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -641,11 +641,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # to full paths should not be read from cache, but redetermined. Thus, we wipe the cache # wipe_cache() # Wipes the easyconfig cache with _easyconfigs_cache.clear() # TODO: implement - # EasyBuild configuration is a singleton, since it should _normally_ only be set up once - # For EasyStack files, it needs to be set up once _per entry_ in the EasyStack file - # Thus, we need to wipe the singleton - # wipe_build_options_singleton() # Should do BuildOptions.__class__.instances.clear() # TODO: impelement - # If EasyConfig specific arguments were supplied in EasyStack file # merge arguments with original command line args if path in opts_per_ec: From 3c0252eae690b675f5c3a9a6358fd6d9c63963e6 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 14:13:14 +0200 Subject: [PATCH 09/53] Move install_latest_eb_release logic to the top of 'rest_of_main', that way we can limit to only one 'if options.easystack' --- easybuild/main.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index b6a5ead860..daf8178d67 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -230,6 +230,15 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi completed. When rest_of_main is called as part of a loop, you don't want to exit, but want to return instead. This boolean controls that behaviour. """ + + if options.install_latest_eb_release: + if orig_paths: + raise EasyBuildError("Installing the latest EasyBuild release can not be combined with installing " + "other easyconfigs") + else: + eb_file = find_easybuild_easyconfig() + orig_paths.append(eb_file) + # Unpack cfg_settings (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, from_pr_list, tweaked_ecs_paths) = cfg_settings @@ -525,9 +534,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): last_log = find_last_log(logfile) or '(none)' print_msg(last_log, log=_log, prefix=False) - # if easystack is provided with the command, commands with arguments from it will be executed - if options.easystack: - orig_paths, opts_per_ec = parse_easystack(options.easystack) # check whether packaging is supported when it's being used if options.package: @@ -618,15 +624,11 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): if not easyconfigs_pkg_paths: _log.warning("Failed to determine install path for easybuild-easyconfigs package.") - if options.install_latest_eb_release: - if orig_paths: - raise EasyBuildError("Installing the latest EasyBuild release can not be combined with installing " - "other easyconfigs") - else: - eb_file = find_easybuild_easyconfig() - orig_paths.append(eb_file) - if options.easystack: + # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file + if options.easystack: + orig_paths, opts_per_ec = parse_easystack(options.easystack) + _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, # we want to know early on, and this loop potentially builds a lot of packages and could take very long From 8afef99d9af118687955aec402eeaca7db39a5cb Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 14:46:42 +0200 Subject: [PATCH 10/53] Get rid of skip_clean_exit. Just always return from rest_of_main where we _used_ to have a clean_exit() call. Right after the call to rest_of_main we call clean_exit anyway. --- easybuild/main.py | 61 +++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index daf8178d67..84fecb4d72 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -213,8 +213,7 @@ def clean_exit(logfile, tmpdir, testing, silent=False): sys.exit(0) -def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build, - skip_clean_exit=False): +def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): """ Remainder of the main function, after orig_paths has been determined @@ -226,9 +225,6 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi :param init_session_state: initial session state, to use in test reports :param hooks: hooks, as loaded by load_hooks from the options :param do_build: whether or not to actually perform the build - :param skip_clean_exit: normally, rest_of_main calls sys.exit(0) whenever certain runs (e.g. a dry-run) have - completed. When rest_of_main is called as part of a loop, you don't want to exit, but want to return instead. - This boolean controls that behaviour. """ if options.install_latest_eb_release: @@ -278,10 +274,7 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi print_msg("Contents of %s:" % path) print_msg(read_file(path), prefix=False) - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing) - else: - return + return True if determined_paths: # transform paths into tuples, use 'False' to indicate the corresponding easyconfig files were not generated @@ -311,10 +304,7 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi # handle --check-contrib & --check-style options if run_contrib_style_checks([ec['ec'] for ec in easyconfigs], options.check_contrib, options.check_style): - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing) - else: - return + return True # verify easyconfig filenames, if desired if options.verify_easyconfig_filenames: @@ -331,10 +321,7 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi if options.containerize: # if --containerize/-C create a container recipe (and optionally container image), and stop containerize(easyconfigs) - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing) - else: - return + return True forced = options.force or options.rebuild dry_run_mode = options.dry_run or options.dry_run_short or options.missing_modules @@ -380,10 +367,7 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi clean_up_easyconfigs(tweaked_ecs_in_all_ecs) copy_files(tweaked_ecs_in_all_ecs, target_path, allow_empty=False, verbose=True) - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing) - else: - return + return True # creating/updating PRs if pr_options: @@ -432,29 +416,20 @@ def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_sessi # cleanup and exit after dry run, searching easyconfigs or submitting regression test stop_options = [options.check_conflicts, dry_run_mode, options.dump_env_script, options.inject_checksums] if any(no_ec_opts) or any(stop_options): - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing) - else: - return + return True # create dependency graph and exit if options.dep_graph: _log.info("Creating dependency graph %s" % options.dep_graph) dep_graph(options.dep_graph, ordered_ecs) - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing, silent=True) - else: - return + return True # submit build as job(s), clean up and exit if options.job: submit_jobs(ordered_ecs, eb_go.generate_cmd_line(), testing=testing) if not testing: print_msg("Submitted parallel build jobs, exiting now") - if not skip_clean_exit: - clean_exit(logfile, eb_tmpdir, testing) - else: - return + return True # build software, will exit when errors occurs (except when testing) if not testing or (testing and do_build): @@ -625,8 +600,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): _log.warning("Failed to determine install path for easybuild-easyconfigs package.") - # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file - if options.easystack: + # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file + if options.easystack: orig_paths, opts_per_ec = parse_easystack(options.easystack) _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) @@ -634,8 +609,10 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # we want to know early on, and this loop potentially builds a lot of packages and could take very long # for path in orig_paths: # validate_command_opts(args, opts_per_ec[path]) + # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it + do_cleanup = True for path in orig_paths: _log.debug("Starting build for %s" % path) # NOTE: not sure if this is needed. Is the EasyConfigs cache preserved throughout loops of this iteration? @@ -662,20 +639,20 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, reconfigure=True) hooks = load_hooks(options.hooks) - overall_success = rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, - hooks, do_build, skip_clean_exit=True) + do_cleanup &= rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, + hooks, do_build) # Loop done. If overall_success is not false, cleanup - if overall_success or overall_success is None: - cleanup(logfile, eb_tmpdir, testing) + # if overall_success or overall_success is None: + # cleanup(logfile, eb_tmpdir, testing) else: - overall_success = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, + do_cleanup = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) - if overall_success: + if do_cleanup: print("Running cleanup") - cleanup(logfile, eb_tmpdir, testing) + clean_exit(logfile, eb_tmpdir, testing, silent=False) if __name__ == "__main__": From ba76b54043772fbaa1d6d953123f3ebb7dca258d Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 14:57:02 +0200 Subject: [PATCH 11/53] Make hound happy --- easybuild/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 29586c5d20..e52ca36dd6 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -604,7 +604,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): if not easyconfigs_pkg_paths: _log.warning("Failed to determine install path for easybuild-easyconfigs package.") - # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file if options.easystack: orig_paths, opts_per_ec = parse_easystack(options.easystack) @@ -641,18 +640,19 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # If no EasyConfig specific arguments are defined, sse original args. # That way,set_up_configuration restores the original config new_args = args - eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, reconfigure=True) + eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, + reconfigure=True) hooks = load_hooks(options.hooks) do_cleanup &= rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, - hooks, do_build) + hooks, do_build) # Loop done. If overall_success is not false, cleanup # if overall_success or overall_success is None: # cleanup(logfile, eb_tmpdir, testing) else: do_cleanup = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, - do_build) + do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if do_cleanup: From e107a4b55670c9386ed518e91cc39dc431fa6267 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:06:47 +0200 Subject: [PATCH 12/53] Renamed rest_of_main to process_eb_args. Fixed some more hound stuff --- easybuild/main.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index e52ca36dd6..842b313731 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -213,11 +213,14 @@ def clean_exit(logfile, tmpdir, testing, silent=False): sys.exit(0) -def rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): +def process_eb_args(args, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): """ - Remainder of the main function, after orig_paths has been determined + Remainder of main function, actually process provided arguments (list of files/paths), + according to specified options. - :param orig_paths: list of EasyConfig names + :param args: list of arguments that were specified to 'eb' command (or an easystack file); + includes filenames/paths of files to process + (mostly easyconfig files, but can also includes patch files, etc.) :param options: eb_go.options, as returned by set_up_configuration :param cfg_settings: as returned by set_up_configuration :param modtool: the modules tool, as returned by modules_tool() @@ -514,7 +517,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): last_log = find_last_log(logfile) or '(none)' print_msg(last_log, log=_log, prefix=False) - # check whether packaging is supported when it's being used if options.package: check_pkg_support() @@ -641,18 +643,18 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # That way,set_up_configuration restores the original config new_args = args eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, - reconfigure=True) + reconfigure=True) hooks = load_hooks(options.hooks) - do_cleanup &= rest_of_main([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, - hooks, do_build) + do_cleanup &= process_eb_args([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, + hooks, do_build) # Loop done. If overall_success is not false, cleanup # if overall_success or overall_success is None: # cleanup(logfile, eb_tmpdir, testing) else: - do_cleanup = rest_of_main(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, - do_build) + do_cleanup = process_eb_args(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, + do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if do_cleanup: From d1298598e45abea579ae25c8a1d00ab827ceee89 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:18:30 +0200 Subject: [PATCH 13/53] Also clear ConfigurationVariables singleton. Furthermore, cleanup the printing of the warning --- easybuild/tools/options.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 6f61bd9db1..87e399ff11 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1594,12 +1594,15 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r # Remove existing singletons if reconfigure==True (allows reconfiguration when looping over EasyStack items) if reconfigure: BuildOptions.__class__._instances.clear() - elif len(BuildOptions.__class__._instances) > 0: - log.warn("set_up_configuration is about to call init() and init_build_options(). " - "However, the singletons that these functions normally initialize already exist. " - "If configuration should be changed, this may lead to unexpected behavior, as the existing " - "singletons will be returned. If you intended to reconfigure, you should probably pass " - "reconfigure=True to set_up_configuration().") + ConfigurationVariables.__class__._instances.clear() + elif len(BuildOptions.__class__._instances) + len(ConfigurationVariables.__class__._instance) > 0: + msg = '\n'.join([ + "set_up_configuration is about to call init() and init_build_options().", + "However, the singletons that these functions normally initialize already exist.", + "If configuration should be changed, this may lead to unexpected behavior," + "as the existing singletons will be returned. If you intended to reconfigure", + "you should probably pass reconfigure=True to set_up_configuration().") + print_warning(msg, log=log) # initialise the EasyBuild configuration & build options init(options, config_options_dict) From 523e47e919436f21f739ea2c31adcbf3bbead2a0 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:23:04 +0200 Subject: [PATCH 14/53] Inline unroll_arguments again, as it used to be. It is only used once anyway --- easybuild/tools/options.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 87e399ff11..30db90307e 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1445,21 +1445,6 @@ def det_location(opt, prefix=''): pretty_print_opts(opts_dict) -def unroll_arguments(args): - # unroll arguments that correspond to a combo of single-letter options - # this is done to avoid interpreting -rD like "--robot D" instead of "--robot -D" - eb_args = [] - letters_regex = re.compile('^[a-zA-Z]+$') - for arg in args: - if len(arg) > 2 and arg.startswith('-') and letters_regex.match(arg[1:]): - for letter in arg[1:]: - eb_args.append('-' + letter) - else: - eb_args.append(arg) - - return eb_args - - def parse_options(args=None, with_include=True): """wrapper function for option parsing""" if os.environ.get('DEBUG_EASYBUILD_OPTIONS', '0').lower() in ('1', 'true', 'yes', 'y'): @@ -1470,7 +1455,16 @@ def parse_options(args=None, with_include=True): if args is None: args = sys.argv[1:] - eb_args = unroll_arguments(args) + # unroll arguments that correspond to a combo of single-letter options + # this is done to avoid interpreting -rD like "--robot D" instead of "--robot -D" + eb_args = [] + letters_regex = re.compile('^[a-zA-Z]+$') + for arg in args: + if len(arg) > 2 and arg.startswith('-') and letters_regex.match(arg[1:]): + for letter in arg[1:]: + eb_args.append('-' + letter) + else: + eb_args.append(arg) usage = "%prog [options] easyconfig [...]" description = ("Builds software based on easyconfig (or parse a directory).\n" From 819138d1d532bf185ba2b63d880e8dfc1fb70ee6 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:42:05 +0200 Subject: [PATCH 15/53] Clear easyconfigs caches in between loop iterations over items in easystack file. Replace orig_paths with eb_args in process_eb_args --- easybuild/main.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 842b313731..7cc1948c17 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -47,6 +47,7 @@ from easybuild.framework.easyblock import build_and_install_one, inject_checksums from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR +from easybuild.framework.easyconfig import easyconfig from easybuild.framework.easystack import parse_easystack from easybuild.framework.easyconfig.easyconfig import clean_up_easyconfigs from easybuild.framework.easyconfig.easyconfig import fix_deprecated_easyconfigs, verify_easyconfig_filename @@ -213,12 +214,12 @@ def clean_exit(logfile, tmpdir, testing, silent=False): sys.exit(0) -def process_eb_args(args, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): +def process_eb_args(eb_args, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): """ Remainder of main function, actually process provided arguments (list of files/paths), according to specified options. - :param args: list of arguments that were specified to 'eb' command (or an easystack file); + :param eb_args: list of arguments that were specified to 'eb' command (or an easystack file); includes filenames/paths of files to process (mostly easyconfig files, but can also includes patch files, etc.) :param options: eb_go.options, as returned by set_up_configuration @@ -231,12 +232,12 @@ def process_eb_args(args, options, cfg_settings, modtool, testing, init_session_ """ if options.install_latest_eb_release: - if orig_paths: + if eb_args: raise EasyBuildError("Installing the latest EasyBuild release can not be combined with installing " "other easyconfigs") else: eb_file = find_easybuild_easyconfig() - orig_paths.append(eb_file) + eb_args.append(eb_file) # Unpack cfg_settings (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, @@ -244,9 +245,9 @@ def process_eb_args(args, options, cfg_settings, modtool, testing, init_session_ if options.copy_ec: # figure out list of files to copy + target location (taking into account --from-pr) - orig_paths, target_path = det_copy_ec_specs(orig_paths, from_pr_list) + eb_args, target_path = det_copy_ec_specs(eb_args, from_pr_list) - categorized_paths = categorize_files_by_type(orig_paths) + categorized_paths = categorize_files_by_type(eb_args) # command line options that do not require any easyconfigs to be specified pr_options = options.new_branch_github or options.new_pr or options.new_pr_from_branch or options.preview_pr @@ -265,7 +266,7 @@ def process_eb_args(args, options, cfg_settings, modtool, testing, init_session_ if options.copy_ec: # at this point some paths may still just be filenames rather than absolute paths, # so try to determine full path for those too via robot search path - paths = locate_files(orig_paths, robot_path) + paths = locate_files(eb_args, robot_path) copy_files(paths, target_path, target_single_file=True, allow_empty=False, verbose=True) @@ -621,10 +622,9 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): do_cleanup = True for path in orig_paths: _log.debug("Starting build for %s" % path) - # NOTE: not sure if this is needed. Is the EasyConfigs cache preserved throughout loops of this iteration? - # Current 'path' may have different options associated with it. Thus, resolution of EasyConfigs - # to full paths should not be read from cache, but redetermined. Thus, we wipe the cache - # wipe_cache() # Wipes the easyconfig cache with _easyconfigs_cache.clear() # TODO: implement + # Whipe easyconfig caches + easyconfig._easyconfigs_cache.clear() + easyconfig._easyconfig_files_cache.clear() # If EasyConfig specific arguments were supplied in EasyStack file # merge arguments with original command line args @@ -639,7 +639,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " "options from the EasyStack file: %s" % (path, new_args)) else: - # If no EasyConfig specific arguments are defined, sse original args. + # If no EasyConfig specific arguments are defined, use original args. # That way,set_up_configuration restores the original config new_args = args eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, @@ -648,10 +648,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): hooks = load_hooks(options.hooks) do_cleanup &= process_eb_args([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, hooks, do_build) - - # Loop done. If overall_success is not false, cleanup - # if overall_success or overall_success is None: - # cleanup(logfile, eb_tmpdir, testing) else: do_cleanup = process_eb_args(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build) From d3f4ac32508a464c71348e92aea10927041ec704 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:47:23 +0200 Subject: [PATCH 16/53] Fix identiation and forgotten bracket --- easybuild/tools/options.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 30db90307e..03b4043e49 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1591,11 +1591,12 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r ConfigurationVariables.__class__._instances.clear() elif len(BuildOptions.__class__._instances) + len(ConfigurationVariables.__class__._instance) > 0: msg = '\n'.join([ - "set_up_configuration is about to call init() and init_build_options().", - "However, the singletons that these functions normally initialize already exist.", - "If configuration should be changed, this may lead to unexpected behavior," - "as the existing singletons will be returned. If you intended to reconfigure", - "you should probably pass reconfigure=True to set_up_configuration().") + "set_up_configuration is about to call init() and init_build_options().", + "However, the singletons that these functions normally initialize already exist.", + "If configuration should be changed, this may lead to unexpected behavior," + "as the existing singletons will be returned. If you intended to reconfigure", + "you should probably pass reconfigure=True to set_up_configuration()." + ]) print_warning(msg, log=log) # initialise the EasyBuild configuration & build options From a656eaa7fdf6f78a0d366cc16d6f5dbe7a08d5f9 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:51:10 +0200 Subject: [PATCH 17/53] Fixed missing import and typo --- easybuild/tools/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index 03b4043e49..bcc71bac65 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -72,7 +72,7 @@ from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS from easybuild.tools.config import OUTPUT_STYLE_AUTO, OUTPUT_STYLES, WARN from easybuild.tools.config import get_pretend_installpath, init, init_build_options, mk_full_default_path -from easybuild.tools.config import BuildOptions +from easybuild.tools.config import BuildOptions, ConfigurationVariables from easybuild.tools.configobj import ConfigObj, ConfigObjError from easybuild.tools.docs import FORMAT_TXT, FORMAT_RST from easybuild.tools.docs import avail_cfgfile_constants, avail_easyconfig_constants, avail_easyconfig_licenses @@ -1589,7 +1589,7 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r if reconfigure: BuildOptions.__class__._instances.clear() ConfigurationVariables.__class__._instances.clear() - elif len(BuildOptions.__class__._instances) + len(ConfigurationVariables.__class__._instance) > 0: + elif len(BuildOptions.__class__._instances) + len(ConfigurationVariables.__class__._instances) > 0: msg = '\n'.join([ "set_up_configuration is about to call init() and init_build_options().", "However, the singletons that these functions normally initialize already exist.", From d98aa4673f9b7c2d46d49e39d6d336e0b5c8e7fe Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 15:52:39 +0200 Subject: [PATCH 18/53] Removed debugging print --- easybuild/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/easybuild/main.py b/easybuild/main.py index 7cc1948c17..1a0dfffa91 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -654,7 +654,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if do_cleanup: - print("Running cleanup") clean_exit(logfile, eb_tmpdir, testing, silent=False) From 5096c4cc324bbd9642de7070cb87cc672a7d3bc0 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 16:26:15 +0200 Subject: [PATCH 19/53] Seperated out processing of the EasyStack file into a single function. Added reinitialization of the modules_tool, in case different items in the EasyStack file use different module tools --- easybuild/main.py | 100 ++++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 1a0dfffa91..cfe44f4595 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -214,6 +214,66 @@ def clean_exit(logfile, tmpdir, testing, silent=False): sys.exit(0) +def process_easystack(easystack_path, args, logfile, testing, init_session_state, do_build): + """ + Process an EasyStack file. That means, parsing, looping over all items in the EasyStack file + building (where requested) the individual items, etc + + :param easystack_path: path to EasyStack file to be processed + :param args: original command line arguments as received by main() + :param logfile: log file to use + :param testing: enable testing mode + :param init_session_state: initial session state, to use in test reports + :param do_build: whether or not to actually perform the build + """ + orig_paths, opts_per_ec = parse_easystack(easystack_path) + + _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) + # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, + # we want to know early on, and this loop potentially builds a lot of packages and could take very long + # for path in orig_paths: + # validate_command_opts(args, opts_per_ec[path]) + + # Loop over each item in the EasyStack file, each time updating the config + # This is because each item in an EasyStack file can have options associated with it + do_cleanup = True + for path in orig_paths: + _log.debug("Starting build for %s" % path) + # Whipe easyconfig caches + easyconfig._easyconfigs_cache.clear() + easyconfig._easyconfig_files_cache.clear() + + # If EasyConfig specific arguments were supplied in EasyStack file + # merge arguments with original command line args + if path in opts_per_ec: + _log.debug("EasyConfig specific options have been specified for " + "%s in the EasyStack file: %s" % (path, opts_per_ec[path])) + if args is None: + args = sys.argv[1:] + ec_args = dict_to_argslist(opts_per_ec[path]) + # By appending ec_args to args, ec_args take priority + new_args = args + ec_args + _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " + "options from the EasyStack file: %s" % (path, new_args)) + else: + # If no EasyConfig specific arguments are defined, use original args. + # That way,set_up_configuration restores the original config + new_args = args + + # Reconfigure + eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, + reconfigure=True) + # Since we reconfigure, we should also reload hooks and get current module tools + hooks = load_hooks(eb_go.options.hooks) + modtool = modules_tool(testing=testing) + + # Process actual item in the EasyStack file + do_cleanup &= process_eb_args([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, + hooks, do_build) + + return do_cleanup + + def process_eb_args(eb_args, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): """ Remainder of main function, actually process provided arguments (list of files/paths), @@ -609,45 +669,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file if options.easystack: - orig_paths, opts_per_ec = parse_easystack(options.easystack) - - _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) - # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, - # we want to know early on, and this loop potentially builds a lot of packages and could take very long - # for path in orig_paths: - # validate_command_opts(args, opts_per_ec[path]) - - # Loop over each item in the EasyStack file, each time updating the config - # This is because each item in an EasyStack file can have options associated with it - do_cleanup = True - for path in orig_paths: - _log.debug("Starting build for %s" % path) - # Whipe easyconfig caches - easyconfig._easyconfigs_cache.clear() - easyconfig._easyconfig_files_cache.clear() - - # If EasyConfig specific arguments were supplied in EasyStack file - # merge arguments with original command line args - if path in opts_per_ec: - _log.debug("EasyConfig specific options have been specified for " - "%s in the EasyStack file: %s" % (path, opts_per_ec[path])) - if args is None: - args = sys.argv[1:] - ec_args = dict_to_argslist(opts_per_ec[path]) - # By appending ec_args to args, ec_args take priority - new_args = args + ec_args - _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " - "options from the EasyStack file: %s" % (path, new_args)) - else: - # If no EasyConfig specific arguments are defined, use original args. - # That way,set_up_configuration restores the original config - new_args = args - eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, - reconfigure=True) - - hooks = load_hooks(options.hooks) - do_cleanup &= process_eb_args([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, - hooks, do_build) + do_cleanup = process_easystack(options.easystack, args, logfile, testing, init_session_state, do_build) else: do_cleanup = process_eb_args(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build) From 63f9a3d5c310064f35eb42a821ff76caee0951f8 Mon Sep 17 00:00:00 2001 From: casparl Date: Wed, 12 Oct 2022 17:33:44 +0200 Subject: [PATCH 20/53] Warn against using EasyStack combined with normal command line arguments, i.e. additional EasyConfig files. This is not supported, as only the things from the EasyStack file will be processed, the EasyConfig files defined as command line arguments will simply be ignored. The warning makes this clear. --- easybuild/main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/easybuild/main.py b/easybuild/main.py index cfe44f4595..768522ea64 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -669,6 +669,13 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file if options.easystack: + if len(orig_paths) > 0: + msg = '\n'.join([ + "Passing additional arguments when building from an EasyStack file is not supported.", + "The following arguments will be ignored:", + *orig_paths, + ]) + print_warning(msg) do_cleanup = process_easystack(options.easystack, args, logfile, testing, init_session_state, do_build) else: do_cleanup = process_eb_args(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, From b11f346604599eea8e1aec6f8f9be4c9dea5382c Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 12 Oct 2022 18:22:25 +0200 Subject: [PATCH 21/53] add test for test_set_up_configuration --- test/framework/options.py | 92 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/test/framework/options.py b/test/framework/options.py index b541c024ae..3ee076cab5 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -48,9 +48,9 @@ from easybuild.framework.easyconfig import MANDATORY, MODULES, OTHER, TOOLCHAIN from easybuild.framework.easyconfig.easyconfig import EasyConfig, get_easyblock_class, robot_find_easyconfig from easybuild.framework.easyconfig.parser import EasyConfigParser -from easybuild.tools.build_log import EasyBuildError -from easybuild.tools.config import DEFAULT_MODULECLASSES -from easybuild.tools.config import find_last_log, get_build_log_path, get_module_syntax, module_classes +from easybuild.tools.build_log import EasyBuildError, EasyBuildLog +from easybuild.tools.config import DEFAULT_MODULECLASSES, BuildOptions, ConfigurationVariables +from easybuild.tools.config import build_option, find_last_log, get_build_log_path, get_module_syntax, module_classes from easybuild.tools.environment import modify_env from easybuild.tools.filetools import adjust_permissions, change_dir, copy_dir, copy_file, download_file from easybuild.tools.filetools import is_patch_file, mkdir, move_file, parse_http_header_fields_urlpat @@ -58,13 +58,14 @@ from easybuild.tools.github import GITHUB_RAW, GITHUB_EB_MAIN, GITHUB_EASYCONFIGS_REPO from easybuild.tools.github import URL_SEPARATOR, fetch_github_token from easybuild.tools.modules import Lmod -from easybuild.tools.options import EasyBuildOptions, parse_external_modules_metadata, set_tmpdir, use_color +from easybuild.tools.options import EasyBuildOptions, parse_external_modules_metadata +from easybuild.tools.options import set_up_configuration, set_tmpdir, use_color from easybuild.tools.py2vs3 import URLError, reload, sort_looseversions from easybuild.tools.toolchain.utilities import TC_CONST_PREFIX from easybuild.tools.run import run_cmd from easybuild.tools.systemtools import HAVE_ARCHSPEC from easybuild.tools.version import VERSION -from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config +from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, cleanup, init_config try: import pycodestyle # noqa @@ -6542,6 +6543,87 @@ def test_easystack_basic(self): regex = re.compile(pattern) self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout)) + def test_set_up_configuration(self): + """Tests for set_up_configuration function.""" + + # check default configuration first + self.assertFalse(build_option('debug')) + self.assertEqual(get_module_syntax(), 'Lua') + self.assertFalse(build_option('hidden')) + + # start with a clean slate, reset all configuration done by setUp method that prepares each test + cleanup() + + os.environ['EASYBUILD_PREFIX'] = self.test_prefix + eb_go, settings = set_up_configuration(args=['--debug', '--module-syntax=Tcl'], silent=True) + + # 2nd part of return value is a tuple with various settings + self.assertTrue(isinstance(settings, tuple)) + self.assertEqual(len(settings), 9) + self.assertEqual(settings[0], {}) # build specs + self.assertTrue(isinstance(settings[1], EasyBuildLog)) # EasyBuildLog instance + self.assertTrue(settings[2].endswith('.log')) # path to log file + self.assertTrue(os.path.exists(settings[2])) + self.assertTrue(isinstance(settings[3], list)) # list of robot paths + self.assertEqual(len(settings[3]), 1) + self.assertTrue(os.path.samefile(settings[3][0], os.environ['EASYBUILD_ROBOT_PATHS'])) + self.assertEqual(settings[4], None) # search query + self.assertTrue(os.path.samefile(settings[5], tempfile.gettempdir())) # tmpdir + self.assertEqual(settings[6], False) # try_to_generate + self.assertEqual(settings[7], []) # from_prs list + self.assertEqual(settings[8], None) # list of paths for tweaked ecs + + self.assertEqual(eb_go.options.prefix, self.test_prefix) + self.assertTrue(eb_go.options.debug) + self.assertEqual(eb_go.options.module_syntax, 'Tcl') + + # set_up_configuration also initializes build options and configuration variables (both Singleton classes) + self.assertTrue(build_option('debug')) + self.assertTrue(BuildOptions()['debug']) + + self.assertEqual(ConfigurationVariables()['module_syntax'], 'Tcl') + self.assertEqual(get_module_syntax(), 'Tcl') + + self.assertFalse(BuildOptions()['hidden']) + self.assertFalse(build_option('hidden')) + + # calling set_up_configuration again triggers a warning being printed, + # because build options and configuration variables will not be re-configured by default! + self.mock_stderr(True) + eb_go, _ = set_up_configuration(args=['--hidden'], silent=True) + stderr = self.get_stderr() + self.mock_stderr(False) + + self.assertTrue("WARNING: set_up_configuration is about to call init() and init_build_options()" in stderr) + + # 'hidden' option is enabled, but corresponding build option is still set to False! + self.assertTrue(eb_go.options.hidden) + self.assertFalse(BuildOptions()['hidden']) + self.assertFalse(build_option('hidden')) + + self.assertEqual(eb_go.options.prefix, self.test_prefix) + + self.assertTrue(build_option('debug')) + self.assertTrue(BuildOptions()['debug']) + + self.assertEqual(ConfigurationVariables()['module_syntax'], 'Tcl') + self.assertEqual(get_module_syntax(), 'Tcl') + + # build options and configuration variables are only re-initialized on demand + eb_go, _ = set_up_configuration(args=['--hidden'], silent=True, reconfigure=True) + + self.assertTrue(eb_go.options.hidden) + self.assertTrue(BuildOptions()['hidden']) + self.assertTrue(build_option('hidden')) + + self.assertEqual(eb_go.options.prefix, self.test_prefix) + + self.assertFalse(build_option('debug')) + self.assertFalse(BuildOptions()['debug']) + + self.assertEqual(ConfigurationVariables()['module_syntax'], 'Lua') + self.assertEqual(get_module_syntax(), 'Lua') + def suite(): """ returns all the testcases in this module """ From 933f94943967d9e97f081e2ce1cc2d5600a02684 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 12 Oct 2022 20:34:11 +0200 Subject: [PATCH 22/53] rename dict_to_argslist to opts_dict_to_eb_opts + make it more robust + add dedicated test for it --- easybuild/main.py | 4 ++-- easybuild/tools/options.py | 27 +++++++++++++++++---------- test/framework/options.py | 27 ++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 768522ea64..4ff2daf9e8 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -67,7 +67,7 @@ from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr from easybuild.tools.hooks import START, END, load_hooks, run_hook from easybuild.tools.modules import modules_tool -from easybuild.tools.options import set_up_configuration, use_color, dict_to_argslist +from easybuild.tools.options import opts_dict_to_eb_opts, set_up_configuration, use_color from easybuild.tools.output import COLOR_GREEN, COLOR_RED, STATUS_BAR, colorize, print_checks, rich_live_cm from easybuild.tools.output import start_progress_bar, stop_progress_bar, update_progress_bar from easybuild.tools.robot import check_conflicts, dry_run, missing_deps, resolve_dependencies, search_easyconfigs @@ -250,7 +250,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state "%s in the EasyStack file: %s" % (path, opts_per_ec[path])) if args is None: args = sys.argv[1:] - ec_args = dict_to_argslist(opts_per_ec[path]) + ec_args = opts_dict_to_eb_opts(opts_per_ec[path]) # By appending ec_args to args, ec_args take priority new_args = args + ec_args _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index bcc71bac65..c52416744d 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -1898,26 +1898,33 @@ def set_tmpdir(tmpdir=None, raise_error=False): return current_tmpdir -def dict_to_argslist(args_dict): +def opts_dict_to_eb_opts(args_dict): """ - Convert a dictionary with key-value arguments to a list similar to sys.argv[1:]. - Can by used to convert e.g. EasyConfig specific options from an EasyStack file to an argument list - :param args_dict: dictionary with options (keys) and parameters (values), e.g. {'from_pr': [1234], 'robot': True} - :return: a list of arguments, similar to what sys.argv[1:] would return + Convert a dictionary with configuration option values to command-line options for the 'eb' command. + Can by used to convert e.g. easyconfig-specific options from an easystack file to a list of strings + that can be fed into the EasyBuild option parser + :param args_dict: dictionary with configuration option values + :return: a list of strings representing command-line options for the 'eb' command """ _log.debug("Converting dictionary %s to argument list" % args_dict) args = [] - for arg in args_dict: + for arg in sorted(args_dict): if len(arg) == 1: prefix = '-' else: prefix = '--' - option = prefix + arg + option = prefix + str(arg) value = args_dict[arg] - args.append(option) - if value is not None: - args.append(value) + if isinstance(value, (list, tuple)): + value = ','.join(str(x) for x in value) + + if value in [True, None]: + args.append(option) + elif value is False: + args.append('--disable-' + option[2:]) + elif value is not None: + args.append(option + '=' + str(value)) _log.debug("Converted dictionary %s to argument list %s" % (args_dict, args)) return args diff --git a/test/framework/options.py b/test/framework/options.py index 3ee076cab5..105e6eb050 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -58,7 +58,7 @@ from easybuild.tools.github import GITHUB_RAW, GITHUB_EB_MAIN, GITHUB_EASYCONFIGS_REPO from easybuild.tools.github import URL_SEPARATOR, fetch_github_token from easybuild.tools.modules import Lmod -from easybuild.tools.options import EasyBuildOptions, parse_external_modules_metadata +from easybuild.tools.options import EasyBuildOptions, opts_dict_to_eb_opts, parse_external_modules_metadata from easybuild.tools.options import set_up_configuration, set_tmpdir, use_color from easybuild.tools.py2vs3 import URLError, reload, sort_looseversions from easybuild.tools.toolchain.utilities import TC_CONST_PREFIX @@ -6624,6 +6624,31 @@ def test_set_up_configuration(self): self.assertEqual(ConfigurationVariables()['module_syntax'], 'Lua') self.assertEqual(get_module_syntax(), 'Lua') + def test_opts_dict_to_eb_opts(self): + """Tests for opts_dict_to_eb_opts.""" + + self.assertEqual(opts_dict_to_eb_opts({}), []) + self.assertEqual(opts_dict_to_eb_opts({'foo': '123'}), ['--foo=123']) + + opts_dict = { + 'module-syntax': 'Tcl', + # multi-value option + 'from-pr': [1234, 2345], + # enabled boolean options + 'robot': None, + 'force': True, + # disabled boolean option + 'debug': False, + } + expected = [ + '--disable-debug', + '--force', + '--from-pr=1234,2345', + '--module-syntax=Tcl', + '--robot', + ] + self.assertEqual(opts_dict_to_eb_opts(opts_dict), expected) + def suite(): """ returns all the testcases in this module """ From 725615640c137fa9b4f04fe27f0457e70f091b7e Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 12 Oct 2022 20:40:15 +0200 Subject: [PATCH 23/53] add end-to-end test for easystack file with easyconfig-specific options --- test/framework/options.py | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/framework/options.py b/test/framework/options.py index 105e6eb050..344797dbb0 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -57,6 +57,7 @@ from easybuild.tools.filetools import read_file, remove_dir, remove_file, which, write_file from easybuild.tools.github import GITHUB_RAW, GITHUB_EB_MAIN, GITHUB_EASYCONFIGS_REPO from easybuild.tools.github import URL_SEPARATOR, fetch_github_token +from easybuild.tools.module_generator import ModuleGeneratorTcl from easybuild.tools.modules import Lmod from easybuild.tools.options import EasyBuildOptions, opts_dict_to_eb_opts, parse_external_modules_metadata from easybuild.tools.options import set_up_configuration, set_tmpdir, use_color @@ -6543,6 +6544,65 @@ def test_easystack_basic(self): regex = re.compile(pattern) self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout)) + def test_easystack_opts(self): + """Test for easystack file that specifies options for specific easyconfigs.""" + + robot_paths = os.environ['EASYBUILD_ROBOT_PATHS'] + hidden_installpath = os.path.join(self.test_installpath, 'hidden') + + test_es_txt = '\n'.join([ + "easyconfigs:", + " - toy-0.0:", + " options:", + " force: True", + " hidden: True", + " installpath: %s" % hidden_installpath, + " - libtoy-0.0:", + " options:", + " force: True", + " robot: ~", + " robot-paths: %s:%s" % (robot_paths, self.test_prefix), + ]) + test_es_path = os.path.join(self.test_prefix, 'test.yml') + write_file(test_es_path, test_es_txt) + + mod_dir = os.path.join(self.test_installpath, 'modules', 'all') + + # touch module file for libtoy, so we can check whether the existing module is replaced + libtoy_mod = os.path.join(mod_dir, 'libtoy', '0.0') + write_file(libtoy_mod, ModuleGeneratorTcl.MODULE_SHEBANG) + + del os.environ['EASYBUILD_INSTALLPATH'] + args = [ + '--experimental', + '--easystack', test_es_path, + '--installpath', self.test_installpath, + ] + self.eb_main(args, do_build=True, raise_error=True, redo_init_config=False) + + mod_ext = '.lua' if get_module_syntax() == 'Lua' else '' + + # make sure that $EBROOTLIBTOY is not defined + if 'EBROOTLIBTOY' in os.environ: + del os.environ['EBROOTLIBTOY'] + + # libtoy module should be installed, module file should at least set EBROOTLIBTOY + mod_dir = os.path.join(self.test_installpath, 'modules', 'all') + mod_path = os.path.join(mod_dir, 'libtoy', '0.0') + mod_ext + self.assertTrue(os.path.exists(mod_path)) + self.modtool.use(mod_dir) + self.modtool.load(['libtoy']) + self.assertTrue(os.path.exists(os.environ['EBROOTLIBTOY'])) + + # module should be hidden and in different install path + mod_path = os.path.join(hidden_installpath, 'modules', 'all', 'toy', '.0.0') + mod_ext + self.assertTrue(os.path.exists(mod_path)) + + # check build options that were put in place for last easyconfig + self.assertFalse(build_option('hidden')) + self.assertTrue(build_option('force')) + self.assertEqual(build_option('robot'), [robot_paths, self.test_prefix]) + def test_set_up_configuration(self): """Tests for set_up_configuration function.""" From a82ef48a9c684878e9c7d7cdc82fb074c674b65c Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Thu, 13 Oct 2022 18:33:36 +0200 Subject: [PATCH 24/53] fix indent in main.py + fix syntax error with Python 2.7 --- easybuild/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 4ff2daf9e8..991b8d9433 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -301,7 +301,7 @@ def process_eb_args(eb_args, options, cfg_settings, modtool, testing, init_sessi # Unpack cfg_settings (build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, - from_pr_list, tweaked_ecs_paths) = cfg_settings + from_pr_list, tweaked_ecs_paths) = cfg_settings if options.copy_ec: # figure out list of files to copy + target location (taking into account --from-pr) @@ -673,8 +673,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): msg = '\n'.join([ "Passing additional arguments when building from an EasyStack file is not supported.", "The following arguments will be ignored:", - *orig_paths, - ]) + ] + orig_paths) print_warning(msg) do_cleanup = process_easystack(options.easystack, args, logfile, testing, init_session_state, do_build) else: From c8d37593f760910242db4c002f0b62ef2a631650 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Thu, 13 Oct 2022 18:38:49 +0200 Subject: [PATCH 25/53] use cleanup rather than clean_exit at the end of main function --- easybuild/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/main.py b/easybuild/main.py index 991b8d9433..4aa4c4311b 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -682,7 +682,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if do_cleanup: - clean_exit(logfile, eb_tmpdir, testing, silent=False) + cleanup(logfile, eb_tmpdir, testing, silent=False) if __name__ == "__main__": From 03b77554f0a8005ec29942368edfe1dbc50f6a99 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Thu, 13 Oct 2022 18:43:40 +0200 Subject: [PATCH 26/53] also pass down EasyBuildOptions into process_eb_args --- easybuild/main.py | 9 +++++---- test/framework/options.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 4aa4c4311b..6d93eb4d7e 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -274,7 +274,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state return do_cleanup -def process_eb_args(eb_args, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): +def process_eb_args(eb_args, eb_go, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): """ Remainder of main function, actually process provided arguments (list of files/paths), according to specified options. @@ -282,7 +282,8 @@ def process_eb_args(eb_args, options, cfg_settings, modtool, testing, init_sessi :param eb_args: list of arguments that were specified to 'eb' command (or an easystack file); includes filenames/paths of files to process (mostly easyconfig files, but can also includes patch files, etc.) - :param options: eb_go.options, as returned by set_up_configuration + :param eb_go: EasyBuildOptions instance (option parser) + :param options: parsed options, as returned by set_up_configuration :param cfg_settings: as returned by set_up_configuration :param modtool: the modules tool, as returned by modules_tool() :param testing: bool whether we're running in test mode @@ -677,8 +678,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): print_warning(msg) do_cleanup = process_easystack(options.easystack, args, logfile, testing, init_session_state, do_build) else: - do_cleanup = process_eb_args(orig_paths, options, cfg_settings, modtool, testing, init_session_state, hooks, - do_build) + do_cleanup = process_eb_args(orig_paths, eb_go, options, cfg_settings, modtool, testing, init_session_state, + hooks, do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if do_cleanup: diff --git a/test/framework/options.py b/test/framework/options.py index 344797dbb0..177352a7de 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -439,7 +439,7 @@ def check_args(job_args, passed_args=None): eb_file, '--job', ] + job_args - outtxt = self.eb_main(args) + outtxt = self.eb_main(args, raise_error=True) job_msg = r"INFO.* Command template for jobs: .* && eb %%\(spec\)s.* %s.*\n" % ' .*'.join(passed_args) assertmsg = "Info log msg with job command template for --job (job_msg: %s, outtxt: %s)" % (job_msg, outtxt) From 046f58171d5ad5faeed62624eb071ca952664619 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Thu, 13 Oct 2022 20:23:15 +0200 Subject: [PATCH 27/53] only pass down EasyBuildOptions into process_eb_args --- easybuild/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 6d93eb4d7e..1b43ed083c 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -268,13 +268,13 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state modtool = modules_tool(testing=testing) # Process actual item in the EasyStack file - do_cleanup &= process_eb_args([path], eb_go.options, cfg_settings, modtool, testing, init_session_state, + do_cleanup &= process_eb_args([path], eb_go, cfg_settings, modtool, testing, init_session_state, hooks, do_build) return do_cleanup -def process_eb_args(eb_args, eb_go, options, cfg_settings, modtool, testing, init_session_state, hooks, do_build): +def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session_state, hooks, do_build): """ Remainder of main function, actually process provided arguments (list of files/paths), according to specified options. @@ -283,7 +283,6 @@ def process_eb_args(eb_args, eb_go, options, cfg_settings, modtool, testing, ini includes filenames/paths of files to process (mostly easyconfig files, but can also includes patch files, etc.) :param eb_go: EasyBuildOptions instance (option parser) - :param options: parsed options, as returned by set_up_configuration :param cfg_settings: as returned by set_up_configuration :param modtool: the modules tool, as returned by modules_tool() :param testing: bool whether we're running in test mode @@ -291,6 +290,7 @@ def process_eb_args(eb_args, eb_go, options, cfg_settings, modtool, testing, ini :param hooks: hooks, as loaded by load_hooks from the options :param do_build: whether or not to actually perform the build """ + options = eb_go.options if options.install_latest_eb_release: if eb_args: @@ -678,7 +678,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): print_warning(msg) do_cleanup = process_easystack(options.easystack, args, logfile, testing, init_session_state, do_build) else: - do_cleanup = process_eb_args(orig_paths, eb_go, options, cfg_settings, modtool, testing, init_session_state, + do_cleanup = process_eb_args(orig_paths, eb_go, cfg_settings, modtool, testing, init_session_state, hooks, do_build) # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) From 863641c039b2743934190d9246a9ae360798e0fd Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Thu, 13 Oct 2022 20:26:51 +0200 Subject: [PATCH 28/53] move determining of easyconfigs_pkg_paths into process_eb_args + avoid shadowing of imported 'easyconfig' module --- easybuild/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 1b43ed083c..ca17140574 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -292,6 +292,11 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session """ options = eb_go.options + # determine easybuild-easyconfigs package install path + easyconfigs_pkg_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR) + if not easyconfigs_pkg_paths: + _log.warning("Failed to determine install path for easybuild-easyconfigs package.") + if options.install_latest_eb_release: if eb_args: raise EasyBuildError("Installing the latest EasyBuild release can not be combined with installing " @@ -374,8 +379,8 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session # verify easyconfig filenames, if desired if options.verify_easyconfig_filenames: _log.info("Verifying easyconfig filenames...") - for easyconfig in easyconfigs: - verify_easyconfig_filename(easyconfig['spec'], easyconfig['ec'], parsed_ec=easyconfig['ec']) + for ec in easyconfigs: + verify_easyconfig_filename(ec['spec'], ec['ec'], parsed_ec=ec['ec']) # tweak obtained easyconfig files, if requested # don't try and tweak anything if easyconfigs were generated, since building a full dep graph will fail @@ -404,8 +409,8 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session # keep track for which easyconfigs we should set the corresponding module as default if options.set_default_module: - for easyconfig in easyconfigs: - easyconfig['ec'].set_default_module = True + for ec in easyconfigs: + ec['ec'].set_default_module = True # determine an order that will allow all specs in the set to build if len(easyconfigs) > 0: @@ -663,11 +668,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): print_warning("Will not run the test step as requested via skip-test-step. " "Consider using ignore-test-failure instead and verify the results afterwards") - # determine easybuild-easyconfigs package install path - easyconfigs_pkg_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR) - if not easyconfigs_pkg_paths: - _log.warning("Failed to determine install path for easybuild-easyconfigs package.") - # if EasyStack file is provided, parse it, and loop over the items in the EasyStack file if options.easystack: if len(orig_paths) > 0: From d5a8071ef744378682741bb53e2bb392e579c1c5 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 09:07:27 +0200 Subject: [PATCH 29/53] specify that _log is a global variable in process_eb_args + process_easystack --- easybuild/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/easybuild/main.py b/easybuild/main.py index ca17140574..5ffee89b18 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -228,6 +228,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state """ orig_paths, opts_per_ec = parse_easystack(easystack_path) + global _log _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, # we want to know early on, and this loop potentially builds a lot of packages and could take very long @@ -292,6 +293,8 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session """ options = eb_go.options + global _log + # determine easybuild-easyconfigs package install path easyconfigs_pkg_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR) if not easyconfigs_pkg_paths: From 8174f0687017408e5de4002b98805368c9f1d980 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 10:00:37 +0200 Subject: [PATCH 30/53] take into account that test suite may be configured to run with 'Tcl' as module syntax --- test/framework/options.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/framework/options.py b/test/framework/options.py index 177352a7de..5d72afdd61 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -6608,8 +6608,9 @@ def test_set_up_configuration(self): # check default configuration first self.assertFalse(build_option('debug')) - self.assertEqual(get_module_syntax(), 'Lua') self.assertFalse(build_option('hidden')) + # tests may be configured to run with Tcl module syntax + self.assertTrue(get_module_syntax() in ('Lua', 'Tcl')) # start with a clean slate, reset all configuration done by setUp method that prepares each test cleanup() @@ -6681,8 +6682,9 @@ def test_set_up_configuration(self): self.assertFalse(build_option('debug')) self.assertFalse(BuildOptions()['debug']) - self.assertEqual(ConfigurationVariables()['module_syntax'], 'Lua') - self.assertEqual(get_module_syntax(), 'Lua') + # tests may be configured to run with Tcl module syntax + self.assertTrue(ConfigurationVariables()['module_syntax'] in ('Lua', 'Tcl')) + self.assertTrue(get_module_syntax() in ('Lua', 'Tcl')) def test_opts_dict_to_eb_opts(self): """Tests for opts_dict_to_eb_opts.""" From 73ab067c7de5d5530b1066505ba0e653bf3fb393 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 10:53:00 +0200 Subject: [PATCH 31/53] drop support for easystack files that use 'software' are top-level key + make sure that value type of easystack is a list, not a dict --- easybuild/framework/easystack.py | 182 ++++--------------------------- 1 file changed, 20 insertions(+), 162 deletions(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index cccf0bc202..2a2cd6e9dc 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -69,31 +69,11 @@ class EasyStack(object): def __init__(self): self.easybuild_version = None self.robot = False - self.software_list = [] self.easyconfigs = [] # A list of easyconfig names. May or may not include .eb extension # A dict where keys are easyconfig names, values are dictionary of options that should be # applied for that easyconfig self.ec_opts = {} - def compose_ec_filenames(self): - """Returns a list of all easyconfig names""" - ec_filenames = [] - - # entries specified via 'software' top-level key - for sw in self.software_list: - full_ec_version = det_full_ec_version({ - 'toolchain': {'name': sw.toolchain_name, 'version': sw.toolchain_version}, - 'version': sw.version, - 'versionsuffix': sw.versionsuffix, - }) - ec_filename = '%s-%s.eb' % (sw.name, full_ec_version) - ec_filenames.append(ec_filename) - - # entries specified via 'easyconfigs' top-level key - for ec in self.easyconfigs: - ec_filenames.append(ec) - return ec_filenames - # flags applicable to all sw (i.e. robot) def get_general_options(self): """Returns general options (flags applicable to all sw (i.e. --robot))""" @@ -129,37 +109,30 @@ def parse(filepath): except yaml.YAMLError as err: raise EasyBuildError("Failed to parse %s: %s" % (filepath, err)) - easystack_data = None - top_keys = ('easyconfigs', 'software') - keys_found = [] - for key in top_keys: - if key in easystack_raw: - keys_found.append(key) - # For now, we don't support mixing multiple top_keys, so check that only one was defined - if len(keys_found) > 1: - keys_string = ', '.join(keys_found) - msg = "Specifying multiple top level keys (%s) " % keys_string - msg += "in one EasyStack file is currently not supported" - msg += ", see %s for documentation." % EASYSTACK_DOC_URL - raise EasyBuildError(msg) - elif len(keys_found) == 0: - msg = "Not a valid EasyStack YAML file: no 'easyconfigs' or 'software' top-level key found" - msg += ", see %s for documentation." % EASYSTACK_DOC_URL - raise EasyBuildError(msg) - else: - key = keys_found[0] + key = 'easyconfigs' + if key in easystack_raw: easystack_data = easystack_raw[key] - - parse_method_name = 'parse_by_' + key - parse_method = getattr(EasyStackParser, 'parse_by_%s' % key, None) - if parse_method is None: - raise EasyBuildError("Easystack parse method '%s' not found!", parse_method_name) + if isinstance(easystack_data, dict): + msg = '\n'.join([ + "Found dict value for '%s' in %s, should be list." % (key, filepath), + "Make sure you use '-' to create list items under '%s', for example:" % key, + " easyconfigs:", + " - example-1.0.eb:", + " ...", + ]) + raise EasyBuildError(msg) + elif not isinstance(easystack_data, list): + raise EasyBuildError("Value type for '%s' in %s should be list, found %s", + key, filepath, type(easystack_data)) + else: + raise EasyBuildError("Top-level key '%s' missing in easystack file %s", key, filepath) # assign general easystack attributes easybuild_version = easystack_raw.get('easybuild_version', None) robot = easystack_raw.get('robot', False) - return parse_method(filepath, easystack_data, easybuild_version=easybuild_version, robot=robot) + return EasyStackParser.parse_by_easyconfigs(filepath, easystack_data, + easybuild_version=easybuild_version, robot=robot) @staticmethod def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=False): @@ -196,119 +169,6 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa return easystack - @staticmethod - def parse_by_software(filepath, software, easybuild_version=None, robot=False): - """ - Parse easystack file with 'software' as top-level key. - """ - - easystack = EasyStack() - - # assign software-specific easystack attributes - for name in software: - # ensure we have a string value (YAML parser returns type = dict - # if levels under the current attribute are present) - check_value(name, "software name") - try: - toolchains = software[name]['toolchains'] - except KeyError: - raise EasyBuildError("Toolchains for software '%s' are not defined in %s", name, filepath) - for toolchain in toolchains: - check_value(toolchain, "software %s" % name) - - if toolchain == 'SYSTEM': - toolchain_name, toolchain_version = 'system', '' - else: - toolchain_parts = toolchain.split('-', 1) - if len(toolchain_parts) == 2: - toolchain_name, toolchain_version = toolchain_parts - elif len(toolchain_parts) == 1: - toolchain_name, toolchain_version = toolchain, '' - else: - raise EasyBuildError("Incorrect toolchain specification for '%s' in %s, too many parts: %s", - name, filepath, toolchain_parts) - - try: - # if version string containts asterisk or labels, raise error (asterisks not supported) - versions = toolchains[toolchain]['versions'] - except TypeError as err: - wrong_structure_err = "An error occurred when interpreting " - wrong_structure_err += "the data for software %s: %s" % (name, err) - raise EasyBuildError(wrong_structure_err) - if '*' in str(versions): - asterisk_err = "EasyStack specifications of '%s' in %s contain asterisk. " - asterisk_err += "Wildcard feature is not supported yet." - raise EasyBuildError(asterisk_err, name, filepath) - - # yaml versions can be in different formats in yaml file - # firstly, check if versions in yaml file are read as a dictionary. - # Example of yaml structure: - # ======================================================================== - # versions: - # '2.25': - # '2.23': - # versionsuffix: '-R-4.0.0' - # ======================================================================== - if isinstance(versions, dict): - for version in versions: - check_value(version, "%s (with %s toolchain)" % (name, toolchain_name)) - if versions[version] is not None: - version_spec = versions[version] - if 'versionsuffix' in version_spec: - versionsuffix = str(version_spec['versionsuffix']) - else: - versionsuffix = '' - if 'exclude-labels' in str(version_spec) or 'include-labels' in str(version_spec): - lab_err = "EasyStack specifications of '%s' in %s " - lab_err += "contain labels. Labels aren't supported yet." - raise EasyBuildError(lab_err, name, filepath) - else: - versionsuffix = '' - - specs = { - 'name': name, - 'toolchain_name': toolchain_name, - 'toolchain_version': toolchain_version, - 'version': version, - 'versionsuffix': versionsuffix, - } - sw = SoftwareSpecs(**specs) - - # append newly created class instance to the list in instance of EasyStack class - easystack.software_list.append(sw) - continue - - elif isinstance(versions, (list, tuple)): - pass - - # multiple lines without ':' is read as a single string; example: - # versions: - # '2.24' - # '2.51' - elif isinstance(versions, string_type): - versions = versions.split() - - # single values like '2.24' should be wrapped in a list - else: - versions = [versions] - - # if version is not a dictionary, versionsuffix is not specified - versionsuffix = '' - - for version in versions: - check_value(version, "%s (with %s toolchain)" % (name, toolchain_name)) - sw = SoftwareSpecs( - name=name, version=version, versionsuffix=versionsuffix, - toolchain_name=toolchain_name, toolchain_version=toolchain_version) - # append newly created class instance to the list in instance of EasyStack class - easystack.software_list.append(sw) - - # assign general easystack attributes - easystack.easybuild_version = easybuild_version - easystack.robot = robot - - return easystack - @only_if_module_is_available('yaml', pkgname='PyYAML') def parse_easystack(filepath): @@ -321,13 +181,11 @@ def parse_easystack(filepath): # class instance which contains all info about planned build easystack = EasyStackParser.parse(filepath) - easyconfig_names = easystack.compose_ec_filenames() - # Disabled general options for now. We weren't using them, and first want support for EasyConfig-specific options. # Then, we need a method to resolve conflicts (specific options should win) # general_options = easystack.get_general_options() - _log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: %s" % ', '.join(sorted(easyconfig_names))) + _log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: %s" % ', '.join(sorted(easystack.easyconfigs))) _log.debug("Using EasyConfig specific options based on the following dict:") _log.debug(easystack.ec_opts) # if len(general_options) != 0: @@ -335,4 +193,4 @@ def parse_easystack(filepath): # else: # _log.debug("No general options were specified in easystack") - return easyconfig_names, easystack.ec_opts + return easystack.easyconfigs, easystack.ec_opts From 60c511e9ee732e5fd205bf266d1f55b411975f47 Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 12:05:32 +0200 Subject: [PATCH 32/53] Removed tests that rely on software-key based EasyStack files --- test/framework/easystack.py | 46 +------------------ .../easystacks/test_easystack_asterisk.yaml | 6 --- .../easystacks/test_easystack_basic.yaml | 22 ++------- .../test_easystack_easyconfigs.yaml | 5 -- .../easystacks/test_easystack_labels.yaml | 7 --- .../test_easystack_wrong_structure.yaml | 6 --- 6 files changed, 6 insertions(+), 86 deletions(-) delete mode 100644 test/framework/easystacks/test_easystack_asterisk.yaml delete mode 100644 test/framework/easystacks/test_easystack_easyconfigs.yaml delete mode 100644 test/framework/easystacks/test_easystack_labels.yaml delete mode 100644 test/framework/easystacks/test_easystack_wrong_structure.yaml diff --git a/test/framework/easystack.py b/test/framework/easystack.py index a0e2595531..2fad5eca4f 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -71,21 +71,6 @@ def test_easystack_basic(self): self.assertEqual(sorted(ec_fns), sorted(expected)) self.assertEqual(opts, {}) - def test_easystack_easyconfigs(self): - """Test for easystack file using 'easyconfigs' key.""" - topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs.yaml') - - ec_fns, opts = parse_easystack(test_easystack) - expected = [ - 'binutils-2.25-GCCcore-4.9.3.eb', - 'binutils-2.26-GCCcore-4.9.3.eb', - 'foss-2018a.eb', - 'toy-0.0-gompi-2018a-test.eb', - ] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - def test_easystack_easyconfigs_with_eb_ext(self): """Test for easystack file using 'easyconfigs' key, where eb extension is included in the easystack file""" topdir = os.path.dirname(os.path.abspath(__file__)) @@ -123,39 +108,10 @@ def test_easystack_easyconfig_opts(self): def test_parse_fail(self): """Test for clean error when easystack file fails to parse.""" test_yml = os.path.join(self.test_prefix, 'test.yml') - write_file(test_yml, 'software: %s') + write_file(test_yml, 'easyconfigs: %s') error_pattern = "Failed to parse .*/test.yml: while scanning for the next token" self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_yml) - def test_easystack_wrong_structure(self): - """Test for --easystack when yaml easystack has wrong structure""" - topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_wrong_structure.yaml') - - expected_err = r"[\S\s]*An error occurred when interpreting the data for software Bioconductor:" - expected_err += r"( 'float' object is not subscriptable[\S\s]*" - expected_err += r"| 'float' object is unsubscriptable" - expected_err += r"| 'float' object has no attribute '__getitem__'[\S\s]*)" - self.assertErrorRegex(EasyBuildError, expected_err, parse_easystack, test_easystack) - - def test_easystack_asterisk(self): - """Test for --easystack when yaml easystack contains asterisk (wildcard)""" - topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_asterisk.yaml') - - expected_err = "EasyStack specifications of 'binutils' in .*/test_easystack_asterisk.yaml contain asterisk. " - expected_err += "Wildcard feature is not supported yet." - - self.assertErrorRegex(EasyBuildError, expected_err, parse_easystack, test_easystack) - - def test_easystack_labels(self): - topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_labels.yaml') - - error_msg = "EasyStack specifications of 'binutils' in .*/test_easystack_labels.yaml contain labels. " - error_msg += "Labels aren't supported yet." - self.assertErrorRegex(EasyBuildError, error_msg, parse_easystack, test_easystack) - def test_check_value(self): """Test check_value function.""" check_value('1.2.3', None) diff --git a/test/framework/easystacks/test_easystack_asterisk.yaml b/test/framework/easystacks/test_easystack_asterisk.yaml deleted file mode 100644 index 7f440636cd..0000000000 --- a/test/framework/easystacks/test_easystack_asterisk.yaml +++ /dev/null @@ -1,6 +0,0 @@ -software: - binutils: - toolchains: - GCCcore-4.9.3: - versions: - "2.11.*" \ No newline at end of file diff --git a/test/framework/easystacks/test_easystack_basic.yaml b/test/framework/easystacks/test_easystack_basic.yaml index 491f113f4a..1d7bfe073d 100644 --- a/test/framework/easystacks/test_easystack_basic.yaml +++ b/test/framework/easystacks/test_easystack_basic.yaml @@ -1,17 +1,5 @@ -software: - binutils: - toolchains: - GCCcore-4.9.3: - versions: - '2.25': - '2.26': - foss: - toolchains: - SYSTEM: - versions: [2018a] - toy: - toolchains: - gompi-2018a: - versions: - '0.0': - versionsuffix: '-test' +easyconfigs: + - binutils-2.25-GCCcore-4.9.3 + - binutils-2.26-GCCcore-4.9.3 + - foss-2018a + - toy-0.0-gompi-2018a-test diff --git a/test/framework/easystacks/test_easystack_easyconfigs.yaml b/test/framework/easystacks/test_easystack_easyconfigs.yaml deleted file mode 100644 index 1d7bfe073d..0000000000 --- a/test/framework/easystacks/test_easystack_easyconfigs.yaml +++ /dev/null @@ -1,5 +0,0 @@ -easyconfigs: - - binutils-2.25-GCCcore-4.9.3 - - binutils-2.26-GCCcore-4.9.3 - - foss-2018a - - toy-0.0-gompi-2018a-test diff --git a/test/framework/easystacks/test_easystack_labels.yaml b/test/framework/easystacks/test_easystack_labels.yaml deleted file mode 100644 index f00db0e249..0000000000 --- a/test/framework/easystacks/test_easystack_labels.yaml +++ /dev/null @@ -1,7 +0,0 @@ -software: - binutils: - toolchains: - GCCcore-4.9.3: - versions: - '3.11': - exclude-labels: arch:aarch64 diff --git a/test/framework/easystacks/test_easystack_wrong_structure.yaml b/test/framework/easystacks/test_easystack_wrong_structure.yaml deleted file mode 100644 index a328b5413b..0000000000 --- a/test/framework/easystacks/test_easystack_wrong_structure.yaml +++ /dev/null @@ -1,6 +0,0 @@ -software: - Bioconductor: - toolchains: - # foss-2020a: - versions: - 3.11 \ No newline at end of file From 3d02f3113a7540313ae7638ae484224d9eb1b654 Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 12:10:20 +0200 Subject: [PATCH 33/53] Removed versions test, which was specific to EasyStacks with software top level keys --- test/framework/easystack.py | 98 ------------------------------------- 1 file changed, 98 deletions(-) diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 2fad5eca4f..46b653eabb 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -124,104 +124,6 @@ def test_check_value(self): error_pattern = r"Value .* \(of type .*\) obtained for is not valid!" self.assertErrorRegex(EasyBuildError, error_pattern, check_value, version, context) - def test_easystack_versions(self): - """Test handling of versions in easystack files.""" - - test_easystack = os.path.join(self.test_prefix, 'test.yml') - tmpl_easystack_txt = '\n'.join([ - "software:", - " foo:", - " toolchains:", - " SYSTEM:", - " versions:", - ]) - - # normal versions, which are not treated special by YAML: no single quotes needed - versions = ('1.2.3', '1.2.30', '2021a', '1.2.3') - for version in versions: - write_file(test_easystack, tmpl_easystack_txt + ' ' + version) - ec_fns, opts = parse_easystack(test_easystack) - self.assertEqual(ec_fns, ['foo-%s.eb' % version]) - self.assertEqual(opts, {}) - - # multiple versions as a list - test_easystack_txt = tmpl_easystack_txt + " [1.2.3, 3.2.1]" - write_file(test_easystack, test_easystack_txt) - ec_fns, opts = parse_easystack(test_easystack) - expected = ['foo-1.2.3.eb', 'foo-3.2.1.eb'] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - - # multiple versions listed with more info - test_easystack_txt = '\n'.join([ - tmpl_easystack_txt, - " 1.2.3:", - " 2021a:", - " 3.2.1:", - " versionsuffix: -foo", - ]) - write_file(test_easystack, test_easystack_txt) - ec_fns, opts = parse_easystack(test_easystack) - expected = ['foo-1.2.3.eb', 'foo-2021a.eb', 'foo-3.2.1-foo.eb'] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - - # versions that get interpreted by YAML as float or int, single quotes required - for version in ('1.2', '123', '3.50', '100', '2.44_01'): - error_pattern = r"Value .* \(of type .*\) obtained for foo \(with system toolchain\) is not valid\!" - - write_file(test_easystack, tmpl_easystack_txt + ' ' + version) - self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) - - # all is fine when wrapping the value in single quotes - write_file(test_easystack, tmpl_easystack_txt + " '" + version + "'") - ec_fns, opts = parse_easystack(test_easystack) - self.assertEqual(ec_fns, ['foo-%s.eb' % version]) - self.assertEqual(opts, {}) - - # one rotten apple in the basket is enough - test_easystack_txt = tmpl_easystack_txt + " [1.2.3, %s, 3.2.1]" % version - write_file(test_easystack, test_easystack_txt) - self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) - - test_easystack_txt = '\n'.join([ - tmpl_easystack_txt, - " 1.2.3:", - " %s:" % version, - " 3.2.1:", - " versionsuffix: -foo", - ]) - write_file(test_easystack, test_easystack_txt) - self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) - - # single quotes to the rescue! - test_easystack_txt = '\n'.join([ - tmpl_easystack_txt, - " 1.2.3:", - " '%s':" % version, - " 3.2.1:", - " versionsuffix: -foo", - ]) - write_file(test_easystack, test_easystack_txt) - ec_fns, opts = parse_easystack(test_easystack) - expected = ['foo-1.2.3.eb', 'foo-%s.eb' % version, 'foo-3.2.1-foo.eb'] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - - # also check toolchain version that could be interpreted as a non-string value... - test_easystack_txt = '\n'.join([ - 'software:', - ' test:', - ' toolchains:', - ' intel-2021.03:', - " versions: [1.2.3, '2.3']", - ]) - write_file(test_easystack, test_easystack_txt) - ec_fns, opts = parse_easystack(test_easystack) - expected = ['test-1.2.3-intel-2021.03.eb', 'test-2.3-intel-2021.03.eb'] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - def suite(): """ returns all the testcases in this module """ From cd8bb71c2432350943711c01b846f25217a81f0a Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 12:34:40 +0200 Subject: [PATCH 34/53] Added tests for easystack formats that forget the '-' when listing easyconfig names. When EasyConfig names end with ':' this leads to a dict-type object, while it leads to a str-object if the names don't end with ':' --- easybuild/framework/easystack.py | 5 +++-- test/framework/easystack.py | 19 +++++++++++++++++++ .../test_easystack_easyconfigs_dict.yaml | 5 +++++ .../test_easystack_easyconfigs_str.yaml | 5 +++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 test/framework/easystacks/test_easystack_easyconfigs_dict.yaml create mode 100644 test/framework/easystacks/test_easystack_easyconfigs_str.yaml diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index 2a2cd6e9dc..f8c9cbb5a1 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -112,9 +112,10 @@ def parse(filepath): key = 'easyconfigs' if key in easystack_raw: easystack_data = easystack_raw[key] - if isinstance(easystack_data, dict): + if isinstance(easystack_data, dict) or isinstance(easystack_data, str): + datatype = 'dict' if isinstance(easystack_data, dict) else 'str' msg = '\n'.join([ - "Found dict value for '%s' in %s, should be list." % (key, filepath), + "Found %s value for '%s' in %s, should be list." % (datatype, key, filepath), "Make sure you use '-' to create list items under '%s', for example:" % key, " easyconfigs:", " - example-1.0.eb:", diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 46b653eabb..a638aedc58 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -86,6 +86,25 @@ def test_easystack_easyconfigs_with_eb_ext(self): self.assertEqual(sorted(ec_fns), sorted(expected)) self.assertEqual(opts, {}) + def test_easystack_easyconfigs_dict(self): + """Test for easystack file where easyconfigs item is parsed as a dict, because easyconfig names are not + prefixed by dashes""" + topdir = os.path.dirname(os.path.abspath(__file__)) + test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs_dict.yaml') + + error_pattern = r"Found dict value for 'easyconfigs' in .* should be list.\nMake sure you use '-' to create .*" + self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) + + def test_easystack_easyconfigs_str(self): + """Test for easystack file where easyconfigs item is parsed as a dict, because easyconfig names are not + prefixed by dashes""" + topdir = os.path.dirname(os.path.abspath(__file__)) + test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs_str.yaml') + + error_pattern = r"Found str value for 'easyconfigs' in .* should be list.\nMake sure you use '-' to create .*" + self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) + + def test_easystack_easyconfig_opts(self): """Test an easystack file using the 'easyconfigs' key, with additonal options for some easyconfigs""" topdir = os.path.dirname(os.path.abspath(__file__)) diff --git a/test/framework/easystacks/test_easystack_easyconfigs_dict.yaml b/test/framework/easystacks/test_easystack_easyconfigs_dict.yaml new file mode 100644 index 0000000000..a6f7d96d78 --- /dev/null +++ b/test/framework/easystacks/test_easystack_easyconfigs_dict.yaml @@ -0,0 +1,5 @@ +easyconfigs: + binutils-2.25-GCCcore-4.9.3: + binutils-2.26-GCCcore-4.9.3: + foss-2018a: + toy-0.0-gompi-2018a-test: diff --git a/test/framework/easystacks/test_easystack_easyconfigs_str.yaml b/test/framework/easystacks/test_easystack_easyconfigs_str.yaml new file mode 100644 index 0000000000..ddfefd0b47 --- /dev/null +++ b/test/framework/easystacks/test_easystack_easyconfigs_str.yaml @@ -0,0 +1,5 @@ +easyconfigs: + binutils-2.25-GCCcore-4.9.3 + binutils-2.26-GCCcore-4.9.3 + foss-2018a + toy-0.0-gompi-2018a-test From 14c9ba1e08109467773ec13601d806693df27f8a Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 12:52:57 +0200 Subject: [PATCH 35/53] Need to decide if the format in test_easystack_basic_dict.yaml is valid. If so, we should change the code to make that pass. If not, we should adapt the test and check the failure --- test/framework/easystack.py | 17 +++++++++++++++++ .../easystacks/test_easystack_basic_dict.yaml | 5 +++++ 2 files changed, 22 insertions(+) create mode 100644 test/framework/easystacks/test_easystack_basic_dict.yaml diff --git a/test/framework/easystack.py b/test/framework/easystack.py index a638aedc58..7e458c323c 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -71,6 +71,23 @@ def test_easystack_basic(self): self.assertEqual(sorted(ec_fns), sorted(expected)) self.assertEqual(opts, {}) + + def test_easystack_basic_dict(self): + """Test for basic easystack file where easyconfig names end with ':'""" + topdir = os.path.dirname(os.path.abspath(__file__)) + test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_basic_dict.yaml') + + ec_fns, opts = parse_easystack(test_easystack) + expected = [ + 'binutils-2.25-GCCcore-4.9.3.eb', + 'binutils-2.26-GCCcore-4.9.3.eb', + 'foss-2018a.eb', + 'toy-0.0-gompi-2018a-test.eb', + ] + self.assertEqual(sorted(ec_fns), sorted(expected)) + self.assertEqual(opts, {}) + + def test_easystack_easyconfigs_with_eb_ext(self): """Test for easystack file using 'easyconfigs' key, where eb extension is included in the easystack file""" topdir = os.path.dirname(os.path.abspath(__file__)) diff --git a/test/framework/easystacks/test_easystack_basic_dict.yaml b/test/framework/easystacks/test_easystack_basic_dict.yaml new file mode 100644 index 0000000000..6a1f839a11 --- /dev/null +++ b/test/framework/easystacks/test_easystack_basic_dict.yaml @@ -0,0 +1,5 @@ +easyconfigs: + - binutils-2.25-GCCcore-4.9.3: + - binutils-2.26-GCCcore-4.9.3: + - foss-2018a: + - toy-0.0-gompi-2018a-test: From 7e2e6d60bc028033c6fe587b49901781e994950c Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 13:58:15 +0200 Subject: [PATCH 36/53] Changed parsing to no longer build a seperate list of easyconfig names, and a dict of options, but to keep these together in a list of tuples. That way, we can rebuild the same software with multiple different options --- easybuild/framework/easystack.py | 28 +++++++++++++++------------- easybuild/main.py | 19 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index f8c9cbb5a1..7158a9fd92 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -69,10 +69,7 @@ class EasyStack(object): def __init__(self): self.easybuild_version = None self.robot = False - self.easyconfigs = [] # A list of easyconfig names. May or may not include .eb extension - # A dict where keys are easyconfig names, values are dictionary of options that should be - # applied for that easyconfig - self.ec_opts = {} + self.easyconfigs = [] # A list of tuples (easyconfig_name, eaysconfig_specific_opts) # flags applicable to all sw (i.e. robot) def get_general_options(self): @@ -147,7 +144,7 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa if isinstance(easyconfig, str): if not easyconfig.endswith('.eb'): easyconfig = easyconfig + '.eb' - easystack.easyconfigs.append(easyconfig) + easystack.easyconfigs.append((easyconfig, None)) elif isinstance(easyconfig, dict): if len(easyconfig) == 1: # Get single key from dictionary 'easyconfig' @@ -157,10 +154,12 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa easyconf_name_with_eb = easyconf_name + '.eb' else: easyconf_name_with_eb = easyconf_name - easystack.easyconfigs.append(easyconf_name_with_eb) - # Add options to the ec_opts dict - if 'options' in easyconfig[easyconf_name].keys(): - easystack.ec_opts[easyconf_name_with_eb] = easyconfig[easyconf_name]['options'] + # Get options + if 'options' in easyconfig[easyconf_name]: + opts = easyconfig[easyconf_name]['options'] + else: + opts = None + easystack.easyconfigs.append((easyconf_name_with_eb, opts)) else: dict_keys = ', '.join(easyconfig.keys()) msg = "Failed to parse easystack file: expected a dictionary with one key (the EasyConfig name), " @@ -186,12 +185,15 @@ def parse_easystack(filepath): # Then, we need a method to resolve conflicts (specific options should win) # general_options = easystack.get_general_options() - _log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: %s" % ', '.join(sorted(easystack.easyconfigs))) - _log.debug("Using EasyConfig specific options based on the following dict:") - _log.debug(easystack.ec_opts) + # TODO: log content of easystack.easyconfigs to _log.debug in a proper format + sorted_list = sorted(easystack.easyconfigs, key=lambda x:x[0]) # Sort list of tuples by first element in the tuple + print(sorted_list) +# _log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: %s" % ', '.join(list(zip(*sorted_list))[0])) +# _log.debug("Using EasyConfig specific options based on the following dict:") +# _log.debug(easystack.ec_opts) # if len(general_options) != 0: # _log.debug("General options for installation are: \n%s" % str(general_options)) # else: # _log.debug("No general options were specified in easystack") - return easystack.easyconfigs, easystack.ec_opts + return easystack.easyconfigs diff --git a/easybuild/main.py b/easybuild/main.py index 5ffee89b18..fadbb7abac 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -226,10 +226,11 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state :param init_session_state: initial session state, to use in test reports :param do_build: whether or not to actually perform the build """ - orig_paths, opts_per_ec = parse_easystack(easystack_path) + easyconfig_opt_tuples = parse_easystack(easystack_path) global _log - _log.debug("Start build loop over items in the EasyStack file: %s" % orig_paths) + # TODO: create proper message in debug that lists the EasyConfig names +#_log.debug("Start build loop over items in the EasyStack file: %s" % list(zip(*easyconfig_opt_tuples))[0]) # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, # we want to know early on, and this loop potentially builds a lot of packages and could take very long # for path in orig_paths: @@ -238,24 +239,24 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it do_cleanup = True - for path in orig_paths: - _log.debug("Starting build for %s" % path) + for easyconfig_opt_tuple in easyconfig_opt_tuples: + _log.debug("Starting build for %s" % easyconfig_opt_tuple[0]) # Whipe easyconfig caches easyconfig._easyconfigs_cache.clear() easyconfig._easyconfig_files_cache.clear() # If EasyConfig specific arguments were supplied in EasyStack file # merge arguments with original command line args - if path in opts_per_ec: + if easyconfig_opt_tuple[1] is not None: _log.debug("EasyConfig specific options have been specified for " - "%s in the EasyStack file: %s" % (path, opts_per_ec[path])) + "%s in the EasyStack file: %s" % (easyconfig_opt_tuple[0], easyconfig_opt_tuple[1])) if args is None: args = sys.argv[1:] - ec_args = opts_dict_to_eb_opts(opts_per_ec[path]) + ec_args = opts_dict_to_eb_opts(easyconfig_opt_tuple[1]) # By appending ec_args to args, ec_args take priority new_args = args + ec_args _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " - "options from the EasyStack file: %s" % (path, new_args)) + "options from the EasyStack file: %s" % (easyconfig_opt_tuple[0], new_args)) else: # If no EasyConfig specific arguments are defined, use original args. # That way,set_up_configuration restores the original config @@ -269,7 +270,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state modtool = modules_tool(testing=testing) # Process actual item in the EasyStack file - do_cleanup &= process_eb_args([path], eb_go, cfg_settings, modtool, testing, init_session_state, + do_cleanup &= process_eb_args([easyconfig_opt_tuple[0]], eb_go, cfg_settings, modtool, testing, init_session_state, hooks, do_build) return do_cleanup From b4cc78b8a0652e1b5c4062008a3d3eaf2c5787d3 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 14:12:23 +0200 Subject: [PATCH 37/53] proper debug logging of parsed easystack --- easybuild/framework/easystack.py | 15 +++++++++++---- easybuild/main.py | 3 +-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index 7158a9fd92..5928186462 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -28,7 +28,10 @@ :author: Denis Kristak (Inuits) :author: Pavel Grochal (Inuits) :author: Kenneth Hoste (HPC-UGent) +:author: Caspar van Leeuwen (SURF) """ +import pprint + from easybuild.base import fancylogger from easybuild.tools.build_log import EasyBuildError from easybuild.tools.filetools import read_file @@ -71,6 +74,12 @@ def __init__(self): self.robot = False self.easyconfigs = [] # A list of tuples (easyconfig_name, eaysconfig_specific_opts) + def __str__(self): + """ + Pretty printing of an EasyStack instance + """ + return pprint.pformat(self.easyconfigs) + # flags applicable to all sw (i.e. robot) def get_general_options(self): """Returns general options (flags applicable to all sw (i.e. --robot))""" @@ -185,10 +194,8 @@ def parse_easystack(filepath): # Then, we need a method to resolve conflicts (specific options should win) # general_options = easystack.get_general_options() - # TODO: log content of easystack.easyconfigs to _log.debug in a proper format - sorted_list = sorted(easystack.easyconfigs, key=lambda x:x[0]) # Sort list of tuples by first element in the tuple - print(sorted_list) -# _log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: %s" % ', '.join(list(zip(*sorted_list))[0])) + _log.debug("Parsed easystack:\n%s" % easystack) + # _log.debug("Using EasyConfig specific options based on the following dict:") # _log.debug(easystack.ec_opts) # if len(general_options) != 0: diff --git a/easybuild/main.py b/easybuild/main.py index fadbb7abac..cb3050e955 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -229,8 +229,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state easyconfig_opt_tuples = parse_easystack(easystack_path) global _log - # TODO: create proper message in debug that lists the EasyConfig names -#_log.debug("Start build loop over items in the EasyStack file: %s" % list(zip(*easyconfig_opt_tuples))[0]) + # TODO: insert fast loop that validates if all command line options are valid. If there are errors in options, # we want to know early on, and this loop potentially builds a lot of packages and could take very long # for path in orig_paths: From 544de8c9538ada8b12e2d5c6bf2cf1af5670a3c6 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 14:28:43 +0200 Subject: [PATCH 38/53] code style fixes in process_easystack --- easybuild/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index cb3050e955..b9f9f38b3e 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -238,24 +238,24 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it do_cleanup = True - for easyconfig_opt_tuple in easyconfig_opt_tuples: - _log.debug("Starting build for %s" % easyconfig_opt_tuple[0]) + for (path, ec_opts) in easyconfig_opt_tuples: + _log.debug("Starting build for %s" % path) # Whipe easyconfig caches easyconfig._easyconfigs_cache.clear() easyconfig._easyconfig_files_cache.clear() # If EasyConfig specific arguments were supplied in EasyStack file # merge arguments with original command line args - if easyconfig_opt_tuple[1] is not None: + if ec_opts is not None: _log.debug("EasyConfig specific options have been specified for " - "%s in the EasyStack file: %s" % (easyconfig_opt_tuple[0], easyconfig_opt_tuple[1])) + "%s in the EasyStack file: %s", path, ec_opts) if args is None: args = sys.argv[1:] - ec_args = opts_dict_to_eb_opts(easyconfig_opt_tuple[1]) + ec_args = opts_dict_to_eb_opts(ec_opts) # By appending ec_args to args, ec_args take priority new_args = args + ec_args _log.info("Argument list for %s after merging command line arguments with EasyConfig specific " - "options from the EasyStack file: %s" % (easyconfig_opt_tuple[0], new_args)) + "options from the EasyStack file: %s", path, new_args) else: # If no EasyConfig specific arguments are defined, use original args. # That way,set_up_configuration restores the original config @@ -269,7 +269,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state modtool = modules_tool(testing=testing) # Process actual item in the EasyStack file - do_cleanup &= process_eb_args([easyconfig_opt_tuple[0]], eb_go, cfg_settings, modtool, testing, init_session_state, + do_cleanup &= process_eb_args([path], eb_go, cfg_settings, modtool, testing, init_session_state, hooks, do_build) return do_cleanup From 63a879d5760be7e93f337a8cb804fac736bdb5ae Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 14:32:52 +0200 Subject: [PATCH 39/53] return full EasyStack instance in parse_easystack --- easybuild/framework/easystack.py | 16 +++++++--------- easybuild/main.py | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index 5928186462..a7c44a1d89 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -72,13 +72,13 @@ class EasyStack(object): def __init__(self): self.easybuild_version = None self.robot = False - self.easyconfigs = [] # A list of tuples (easyconfig_name, eaysconfig_specific_opts) + self.ec_opt_tuples = [] # A list of tuples (easyconfig_name, eaysconfig_specific_opts) def __str__(self): """ Pretty printing of an EasyStack instance """ - return pprint.pformat(self.easyconfigs) + return pprint.pformat(self.ec_opt_tuples) # flags applicable to all sw (i.e. robot) def get_general_options(self): @@ -153,7 +153,7 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa if isinstance(easyconfig, str): if not easyconfig.endswith('.eb'): easyconfig = easyconfig + '.eb' - easystack.easyconfigs.append((easyconfig, None)) + easystack.ec_opt_tuples.append((easyconfig, None)) elif isinstance(easyconfig, dict): if len(easyconfig) == 1: # Get single key from dictionary 'easyconfig' @@ -164,11 +164,9 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa else: easyconf_name_with_eb = easyconf_name # Get options - if 'options' in easyconfig[easyconf_name]: - opts = easyconfig[easyconf_name]['options'] - else: - opts = None - easystack.easyconfigs.append((easyconf_name_with_eb, opts)) + ec_dict = easyconfig[easyconf_name] or {} + opts = ec_dict.get('options') + easystack.ec_opt_tuples.append((easyconf_name_with_eb, opts)) else: dict_keys = ', '.join(easyconfig.keys()) msg = "Failed to parse easystack file: expected a dictionary with one key (the EasyConfig name), " @@ -203,4 +201,4 @@ def parse_easystack(filepath): # else: # _log.debug("No general options were specified in easystack") - return easystack.easyconfigs + return easystack diff --git a/easybuild/main.py b/easybuild/main.py index b9f9f38b3e..3d758a58f4 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -226,7 +226,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state :param init_session_state: initial session state, to use in test reports :param do_build: whether or not to actually perform the build """ - easyconfig_opt_tuples = parse_easystack(easystack_path) + easystack = parse_easystack(easystack_path) global _log @@ -238,7 +238,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state # Loop over each item in the EasyStack file, each time updating the config # This is because each item in an EasyStack file can have options associated with it do_cleanup = True - for (path, ec_opts) in easyconfig_opt_tuples: + for (path, ec_opts) in easystack.ec_opt_tuples: _log.debug("Starting build for %s" % path) # Whipe easyconfig caches easyconfig._easyconfigs_cache.clear() From a5ce7d2a26592831ed04c4c5b46b6a38ef63ccc5 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 14:32:59 +0200 Subject: [PATCH 40/53] fix test_easystack_basic_dict --- test/framework/easystack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 7e458c323c..17577239f3 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -77,15 +77,15 @@ def test_easystack_basic_dict(self): topdir = os.path.dirname(os.path.abspath(__file__)) test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_basic_dict.yaml') - ec_fns, opts = parse_easystack(test_easystack) + easystack = parse_easystack(test_easystack) expected = [ 'binutils-2.25-GCCcore-4.9.3.eb', 'binutils-2.26-GCCcore-4.9.3.eb', 'foss-2018a.eb', 'toy-0.0-gompi-2018a-test.eb', ] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) + self.assertEqual(sorted([x[0] for x in easystack.ec_opt_tuples]), sorted(expected)) + self.assertTrue(all(x[1] is None for x in easystack.ec_opt_tuples)) def test_easystack_easyconfigs_with_eb_ext(self): From 664ed0937b0b5b0be0cacebf746d79d12286232a Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 14:57:15 +0200 Subject: [PATCH 41/53] fix easystack tests --- easybuild/main.py | 1 + test/framework/easystack.py | 58 +++++++++---------------------------- test/framework/options.py | 10 ++++--- 3 files changed, 21 insertions(+), 48 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index 3d758a58f4..5cccac42b0 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -683,6 +683,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None): else: do_cleanup = process_eb_args(orig_paths, eb_go, cfg_settings, modtool, testing, init_session_state, hooks, do_build) + # stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir) stop_logging(logfile, logtostdout=options.logtostdout) if do_cleanup: diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 17577239f3..6bc31c591a 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -57,25 +57,16 @@ def tearDown(self): super(EasyStackTest, self).tearDown() def test_easystack_basic(self): - """Test for basic easystack file.""" + """Test for basic easystack files.""" topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_basic.yaml') - ec_fns, opts = parse_easystack(test_easystack) - expected = [ - 'binutils-2.25-GCCcore-4.9.3.eb', - 'binutils-2.26-GCCcore-4.9.3.eb', - 'foss-2018a.eb', - 'toy-0.0-gompi-2018a-test.eb', + test_easystacks = [ + 'test_easystack_basic.yaml', + 'test_easystack_basic_dict.yaml', + 'test_easystack_easyconfigs_with_eb_ext.yaml', ] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - - - def test_easystack_basic_dict(self): - """Test for basic easystack file where easyconfig names end with ':'""" - topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_basic_dict.yaml') + for fn in test_easystacks: + test_easystack = os.path.join(topdir, 'easystacks', fn) easystack = parse_easystack(test_easystack) expected = [ @@ -87,22 +78,6 @@ def test_easystack_basic_dict(self): self.assertEqual(sorted([x[0] for x in easystack.ec_opt_tuples]), sorted(expected)) self.assertTrue(all(x[1] is None for x in easystack.ec_opt_tuples)) - - def test_easystack_easyconfigs_with_eb_ext(self): - """Test for easystack file using 'easyconfigs' key, where eb extension is included in the easystack file""" - topdir = os.path.dirname(os.path.abspath(__file__)) - test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs_with_eb_ext.yaml') - - ec_fns, opts = parse_easystack(test_easystack) - expected = [ - 'binutils-2.25-GCCcore-4.9.3.eb', - 'binutils-2.26-GCCcore-4.9.3.eb', - 'foss-2018a.eb', - 'toy-0.0-gompi-2018a-test.eb', - ] - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, {}) - def test_easystack_easyconfigs_dict(self): """Test for easystack file where easyconfigs item is parsed as a dict, because easyconfig names are not prefixed by dashes""" @@ -127,19 +102,14 @@ def test_easystack_easyconfig_opts(self): topdir = os.path.dirname(os.path.abspath(__file__)) test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs_opts.yaml') - ec_fns, opts = parse_easystack(test_easystack) - expected = [ - 'binutils-2.25-GCCcore-4.9.3.eb', - 'binutils-2.26-GCCcore-4.9.3.eb', - 'foss-2018a.eb', - 'toy-0.0-gompi-2018a-test.eb', + easystack = parse_easystack(test_easystack) + expected_tuples = [ + ('binutils-2.25-GCCcore-4.9.3.eb', {'debug': True, 'from-pr': 12345}), + ('binutils-2.26-GCCcore-4.9.3.eb', None), + ('foss-2018a.eb', {'enforce-checksums': True, 'robot': True}), + ('toy-0.0-gompi-2018a-test.eb', None), ] - expected_opts = { - 'binutils-2.25-GCCcore-4.9.3.eb': {'debug': True, 'from-pr': 12345}, - 'foss-2018a.eb': {'enforce-checksums': True, 'robot': True}, - } - self.assertEqual(sorted(ec_fns), sorted(expected)) - self.assertEqual(opts, expected_opts) + self.assertEqual(easystack.ec_opt_tuples, expected_tuples) def test_parse_fail(self): """Test for clean error when easystack file fails to parse.""" diff --git a/test/framework/options.py b/test/framework/options.py index 5d72afdd61..6a86a82967 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -6531,10 +6531,12 @@ def test_easystack_basic(self): args = ['--easystack', toy_easystack, '--debug', '--experimental', '--dry-run'] stdout = self.eb_main(args, do_build=True, raise_error=True) patterns = [ - r"[\S\s]*INFO Building from easystack:[\S\s]*", - r"[\S\s]*DEBUG EasyStack parsed\. Proceeding to install these Easyconfigs: " - r"binutils-2.25-GCCcore-4.9.3.eb, binutils-2.26-GCCcore-4.9.3.eb, " - r"foss-2018a.eb, toy-0.0-gompi-2018a-test.eb", + r"INFO Building from easystack:", + r"DEBUG Parsed easystack:\n" + ".*binutils-2.25-GCCcore-4.9.3.eb.*\n" + ".*binutils-2.26-GCCcore-4.9.3.eb.*\n" + ".*foss-2018a.eb.*\n" + ".*toy-0.0-gompi-2018a-test.eb.*", r"\* \[ \] .*/test_ecs/b/binutils/binutils-2.25-GCCcore-4.9.3.eb \(module: binutils/2.25-GCCcore-4.9.3\)", r"\* \[ \] .*/test_ecs/b/binutils/binutils-2.26-GCCcore-4.9.3.eb \(module: binutils/2.26-GCCcore-4.9.3\)", r"\* \[ \] .*/test_ecs/t/toy/toy-0.0-gompi-2018a-test.eb \(module: toy/0.0-gompi-2018a-test\)", From 761c365ffb4ec218d334c90229604e98431caa17 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 15:08:25 +0200 Subject: [PATCH 42/53] fix iterations in test_easystack_basic --- test/framework/easystack.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 6bc31c591a..d1fbe39ef5 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -68,15 +68,15 @@ def test_easystack_basic(self): for fn in test_easystacks: test_easystack = os.path.join(topdir, 'easystacks', fn) - easystack = parse_easystack(test_easystack) - expected = [ - 'binutils-2.25-GCCcore-4.9.3.eb', - 'binutils-2.26-GCCcore-4.9.3.eb', - 'foss-2018a.eb', - 'toy-0.0-gompi-2018a-test.eb', - ] - self.assertEqual(sorted([x[0] for x in easystack.ec_opt_tuples]), sorted(expected)) - self.assertTrue(all(x[1] is None for x in easystack.ec_opt_tuples)) + easystack = parse_easystack(test_easystack) + expected = [ + 'binutils-2.25-GCCcore-4.9.3.eb', + 'binutils-2.26-GCCcore-4.9.3.eb', + 'foss-2018a.eb', + 'toy-0.0-gompi-2018a-test.eb', + ] + self.assertEqual(sorted([x[0] for x in easystack.ec_opt_tuples]), sorted(expected)) + self.assertTrue(all(x[1] is None for x in easystack.ec_opt_tuples)) def test_easystack_easyconfigs_dict(self): """Test for easystack file where easyconfigs item is parsed as a dict, because easyconfig names are not From fe59b1021b42ec40c5c50c70a2fa4e971c3a493b Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 15:11:08 +0200 Subject: [PATCH 43/53] make sure only 'options' key is used in EasyStackParser.parse_by_easyconfigs --- easybuild/framework/easystack.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index a7c44a1d89..d358da0bc2 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -124,8 +124,10 @@ def parse(filepath): "Found %s value for '%s' in %s, should be list." % (datatype, key, filepath), "Make sure you use '-' to create list items under '%s', for example:" % key, " easyconfigs:", - " - example-1.0.eb:", - " ...", + " - example-1.0.eb", + " - example-2.0.eb:", + " options:" + " ...", ]) raise EasyBuildError(msg) elif not isinstance(easystack_data, list): @@ -165,6 +167,12 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa easyconf_name_with_eb = easyconf_name # Get options ec_dict = easyconfig[easyconf_name] or {} + + # make sure only 'options' key is used (for now) + if any(x != 'options' for x in ec_dict): + msg = "Found one or more invalid keys for %s (only 'options' supported): %s" + raise EasyBuildError(msg, ', '.join(ec_dict.keys())) + opts = ec_dict.get('options') easystack.ec_opt_tuples.append((easyconf_name_with_eb, opts)) else: From d207013246885a6339cb22a92aa68bb62fa5b74d Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 15:14:57 +0200 Subject: [PATCH 44/53] fix typo --- easybuild/framework/easystack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index d358da0bc2..bb145a6503 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -171,7 +171,7 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa # make sure only 'options' key is used (for now) if any(x != 'options' for x in ec_dict): msg = "Found one or more invalid keys for %s (only 'options' supported): %s" - raise EasyBuildError(msg, ', '.join(ec_dict.keys())) + raise EasyBuildError(msg, easyconf_name, ', '.join(ec_dict.keys())) opts = ec_dict.get('options') easystack.ec_opt_tuples.append((easyconf_name_with_eb, opts)) From b0f856f894f33382cebc3ffaa18891023c1de0a4 Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 15:29:55 +0200 Subject: [PATCH 45/53] Silence the notification about the temporary log when reconfiguring --- easybuild/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/main.py b/easybuild/main.py index 5cccac42b0..fb3d37211d 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -263,7 +263,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state # Reconfigure eb_go, cfg_settings = set_up_configuration(args=new_args, logfile=logfile, testing=testing, - reconfigure=True) + reconfigure=True, silent=True) # Since we reconfigure, we should also reload hooks and get current module tools hooks = load_hooks(eb_go.options.hooks) modtool = modules_tool(testing=testing) From fbd61e7507accdfcc451c76295942310f620de25 Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 15:31:18 +0200 Subject: [PATCH 46/53] sort keys in Error --- easybuild/framework/easystack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index bb145a6503..d58484219c 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -171,7 +171,7 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa # make sure only 'options' key is used (for now) if any(x != 'options' for x in ec_dict): msg = "Found one or more invalid keys for %s (only 'options' supported): %s" - raise EasyBuildError(msg, easyconf_name, ', '.join(ec_dict.keys())) + raise EasyBuildError(msg, easyconf_name, ', '.join(sorted(ec_dict.keys()))) opts = ec_dict.get('options') easystack.ec_opt_tuples.append((easyconf_name_with_eb, opts)) From 40ef1ba410658bf5a5b7807d95f4b9ec4a2d380b Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 15:34:32 +0200 Subject: [PATCH 47/53] Added tests to check failures when invalid keys are specified --- test/framework/easystack.py | 17 +++++++++++++++++ .../easystacks/test_easystack_invalid_key.yaml | 14 ++++++++++++++ .../easystacks/test_easystack_invalid_key2.yaml | 14 ++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 test/framework/easystacks/test_easystack_invalid_key.yaml create mode 100644 test/framework/easystacks/test_easystack_invalid_key2.yaml diff --git a/test/framework/easystack.py b/test/framework/easystack.py index d1fbe39ef5..6dca6f0131 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -111,6 +111,23 @@ def test_easystack_easyconfig_opts(self): ] self.assertEqual(easystack.ec_opt_tuples, expected_tuples) + def test_easystack_invalid_key(self): + """Test easystack files with invalid key at the same level as the 'options' key""" + topdir = os.path.dirname(os.path.abspath(__file__)) + test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_invalid_key.yaml') + + error_pattern = r"Found one or more invalid keys for .* \(only 'options' supported\).*" + self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) + + def test_easystack_invalid_key2(self): + """Test easystack files with invalid key at the same level as the key that names the easyconfig""" + topdir = os.path.dirname(os.path.abspath(__file__)) + test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_invalid_key2.yaml') + + error_pattern = r"expected a dictionary with one key \(the EasyConfig name\), " + error_pattern += r"instead found keys: .*, invalid_key" + self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) + def test_parse_fail(self): """Test for clean error when easystack file fails to parse.""" test_yml = os.path.join(self.test_prefix, 'test.yml') diff --git a/test/framework/easystacks/test_easystack_invalid_key.yaml b/test/framework/easystacks/test_easystack_invalid_key.yaml new file mode 100644 index 0000000000..ad4dfd3d71 --- /dev/null +++ b/test/framework/easystacks/test_easystack_invalid_key.yaml @@ -0,0 +1,14 @@ +easyconfigs: + - binutils-2.25-GCCcore-4.9.3: + options: { + 'debug': True, + 'from-pr': 12345, + } + invalid_key: {} + - binutils-2.26-GCCcore-4.9.3 + - foss-2018a: + options: { + 'enforce-checksums': True, + 'robot': True, + } + - toy-0.0-gompi-2018a-test diff --git a/test/framework/easystacks/test_easystack_invalid_key2.yaml b/test/framework/easystacks/test_easystack_invalid_key2.yaml new file mode 100644 index 0000000000..5016194248 --- /dev/null +++ b/test/framework/easystacks/test_easystack_invalid_key2.yaml @@ -0,0 +1,14 @@ +easyconfigs: + - binutils-2.25-GCCcore-4.9.3: + options: { + 'debug': True, + 'from-pr': 12345, + } + invalid_key: {} + - binutils-2.26-GCCcore-4.9.3 + - foss-2018a: + options: { + 'enforce-checksums': True, + 'robot': True, + } + - toy-0.0-gompi-2018a-test From ec4f0fd6023a7da3649eac8124225908c65c63d7 Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 16:15:13 +0200 Subject: [PATCH 48/53] Added test with EasyConfig file htat is missing easyconfigs top level keyword alltogether --- test/framework/easystack.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 6dca6f0131..1481d38f5a 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -128,6 +128,14 @@ def test_easystack_invalid_key2(self): error_pattern += r"instead found keys: .*, invalid_key" self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) + def test_missing_easyconfigs_key(self): + """Test that EasyStack file that doesn't contain an EasyConfigs key will fail with sane error message""" + topdir = os.path.dirname(os.path.abspath(__file__)) + test_easystack = os.path.join(topdir, 'easystacks', 'test_missing_easyconfigs_key.yaml') + + error_pattern = r"Top-level key 'easyconfigs' missing in easystack file %s" % test_easystack + self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) + def test_parse_fail(self): """Test for clean error when easystack file fails to parse.""" test_yml = os.path.join(self.test_prefix, 'test.yml') From e2fbdd22a85477d5897bc83f66450da0c7d458f8 Mon Sep 17 00:00:00 2001 From: casparl Date: Fri, 21 Oct 2022 16:15:38 +0200 Subject: [PATCH 49/53] Added test with EasyConfig file that is missing easyconfigs top level keyword alltogether --- test/framework/easystacks/test_missing_easyconfigs_key.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 test/framework/easystacks/test_missing_easyconfigs_key.yaml diff --git a/test/framework/easystacks/test_missing_easyconfigs_key.yaml b/test/framework/easystacks/test_missing_easyconfigs_key.yaml new file mode 100644 index 0000000000..d8be11fdfa --- /dev/null +++ b/test/framework/easystacks/test_missing_easyconfigs_key.yaml @@ -0,0 +1,4 @@ +- binutils-2.25-GCCcore-4.9.3.eb +- binutils-2.26-GCCcore-4.9.3.eb +- foss-2018a.eb +- toy-0.0-gompi-2018a-test.eb From 292498f98afc249e1927ce554b731dca15f5ae47 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 16:20:17 +0200 Subject: [PATCH 50/53] add test for processing an easystack file that relies on wiping of easyconfig caches in process_easystack --- easybuild/main.py | 2 +- test/framework/options.py | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/easybuild/main.py b/easybuild/main.py index fb3d37211d..25ce186018 100644 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -240,7 +240,7 @@ def process_easystack(easystack_path, args, logfile, testing, init_session_state do_cleanup = True for (path, ec_opts) in easystack.ec_opt_tuples: _log.debug("Starting build for %s" % path) - # Whipe easyconfig caches + # wipe easyconfig caches easyconfig._easyconfigs_cache.clear() easyconfig._easyconfig_files_cache.clear() diff --git a/test/framework/options.py b/test/framework/options.py index 6a86a82967..9488b42b9e 100644 --- a/test/framework/options.py +++ b/test/framework/options.py @@ -6605,6 +6605,49 @@ def test_easystack_opts(self): self.assertTrue(build_option('force')) self.assertEqual(build_option('robot'), [robot_paths, self.test_prefix]) + def test_easystack_easyconfigs_cache(self): + """ + Test for easystack file that specifies same easyconfig twice, + but from a different location. + """ + topdir = os.path.abspath(os.path.dirname(__file__)) + libtoy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 'l', 'libtoy', 'libtoy-0.0.eb') + toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb') + + test_ec = os.path.join(self.test_prefix, 'toy-0.0.eb') + test_ec_txt = read_file(toy_ec) + test_ec_txt += "\ndependencies = [('libtoy', '0.0')]" + write_file(test_ec, test_ec_txt) + + test_subdir = os.path.join(self.test_prefix, 'deps') + mkdir(test_subdir, parents=True) + copy_file(libtoy_ec, test_subdir) + + test_es_txt = '\n'.join([ + "easyconfigs:", + " - toy-0.0", + " - toy-0.0:", + " options:", + " robot: %s:%s" % (test_subdir, self.test_prefix), + ]) + test_es_path = os.path.join(self.test_prefix, 'test.yml') + write_file(test_es_path, test_es_txt) + + args = [ + '--experimental', + '--easystack', test_es_path, + '--dry-run', + '--robot=%s' % self.test_prefix, + ] + stdout = self.eb_main(args, do_build=True, raise_error=True, redo_init_config=False) + + # check whether libtoy-0.0.eb comes from 2nd + regex = re.compile(r"^ \* \[ \] %s" % libtoy_ec, re.M) + self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout)) + + regex = re.compile(r"^ \* \[ \] %s" % os.path.join(test_subdir, 'libtoy-0.0.eb'), re.M) + self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout)) + def test_set_up_configuration(self): """Tests for set_up_configuration function.""" From dfcb2e24f093a8333d490a537c185a6ca00bdd78 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Fri, 21 Oct 2022 22:48:38 +0200 Subject: [PATCH 51/53] remove unused import of det_full_ec_version in framework/easystack.py + remove duplicate blank line in easystack test suite --- easybuild/framework/easystack.py | 1 - test/framework/easystack.py | 1 - 2 files changed, 2 deletions(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index d58484219c..1268e8484d 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -35,7 +35,6 @@ from easybuild.base import fancylogger from easybuild.tools.build_log import EasyBuildError from easybuild.tools.filetools import read_file -from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version from easybuild.tools.py2vs3 import string_type from easybuild.tools.utilities import only_if_module_is_available try: diff --git a/test/framework/easystack.py b/test/framework/easystack.py index 1481d38f5a..f9b971428d 100644 --- a/test/framework/easystack.py +++ b/test/framework/easystack.py @@ -96,7 +96,6 @@ def test_easystack_easyconfigs_str(self): error_pattern = r"Found str value for 'easyconfigs' in .* should be list.\nMake sure you use '-' to create .*" self.assertErrorRegex(EasyBuildError, error_pattern, parse_easystack, test_easystack) - def test_easystack_easyconfig_opts(self): """Test an easystack file using the 'easyconfigs' key, with additonal options for some easyconfigs""" topdir = os.path.dirname(os.path.abspath(__file__)) From b93dd22ffd710fcbe5f306aa52b4e03c64f5a039 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 23 Nov 2022 09:06:27 +0100 Subject: [PATCH 52/53] use more YAML-like syntax for options in test easystack file --- .../easystacks/test_easystack_easyconfigs_opts.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/framework/easystacks/test_easystack_easyconfigs_opts.yaml b/test/framework/easystacks/test_easystack_easyconfigs_opts.yaml index d2bc703986..c8686466a1 100644 --- a/test/framework/easystacks/test_easystack_easyconfigs_opts.yaml +++ b/test/framework/easystacks/test_easystack_easyconfigs_opts.yaml @@ -6,8 +6,9 @@ easyconfigs: } - binutils-2.26-GCCcore-4.9.3 - foss-2018a: - options: { - 'enforce-checksums': True, - 'robot': True, - } + # no strict need for quotes as long as YAML parser doesn't get smart with value type (booleans, integers, ...); + # { and } are also optional in YAML to create a dictionary + options: + enforce-checksums: True + robot: True - toy-0.0-gompi-2018a-test From f4f486d78f1a1fd5bb7e4f8116ead830523afc09 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 23 Nov 2022 11:15:02 +0100 Subject: [PATCH 53/53] sort keys in easystack parser error to avoid failing test due to randomly changing order of keys in older Python versions --- easybuild/framework/easystack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py index 1268e8484d..94f81c9d9c 100644 --- a/easybuild/framework/easystack.py +++ b/easybuild/framework/easystack.py @@ -175,7 +175,7 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa opts = ec_dict.get('options') easystack.ec_opt_tuples.append((easyconf_name_with_eb, opts)) else: - dict_keys = ', '.join(easyconfig.keys()) + dict_keys = ', '.join(sorted(easyconfig.keys())) msg = "Failed to parse easystack file: expected a dictionary with one key (the EasyConfig name), " msg += "instead found keys: %s" % dict_keys msg += ", see %s for documentation." % EASYSTACK_DOC_URL