Repository: easybuilders/easybuild-framework

Issue:
Allow to skip extensions in sanity check
In #3645 the idea was raised to be able to skip the sanity check for extensions only, which might be useful if you are doing `--module-only --rebuild`


Discussion:

Resolving patch:
diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py
--- a/easybuild/framework/easyblock.py
+++ b/easybuild/framework/easyblock.py
@@ -2899,9 +2899,12 @@ def _sanity_check_step_extensions(self):
         """Sanity check on extensions (if any)."""
         failed_exts = []
 
-        # class instances for extensions may not be initialized yet here,
-        # for example when using --module-only or --sanity-check-only
-        if not self.ext_instances:
+        if build_option('skip_extensions'):
+            self.log.info("Skipping sanity check for extensions since skip-extensions is enabled...")
+            return
+        elif not self.ext_instances:
+            # class instances for extensions may not be initialized yet here,
+            # for example when using --module-only or --sanity-check-only
             self.prepare_for_extensions()
             self.init_ext_instances()
 
@@ -3293,6 +3296,8 @@ def skip_step(self, step, skippable):
         force = build_option('force')
         module_only = build_option('module_only')
         sanity_check_only = build_option('sanity_check_only')
+        skip_extensions = build_option('skip_extensions')
+        skip_test_step = build_option('skip_test_step')
         skipsteps = self.cfg['skipsteps']
 
         # under --skip, sanity check is not skipped
@@ -3319,10 +3324,19 @@ def skip_step(self, step, skippable):
             self.log.info("Skipping %s step because of sanity-check-only mode", step)
             skip = True
 
+        elif skip_extensions and step == EXTENSIONS_STEP:
+            self.log.info("Skipping %s step as requested via skip-extensions", step)
+            skip = True
+
+        elif skip_test_step and step == TEST_STEP:
+            self.log.info("Skipping %s step as requested via skip-test-step", step)
+            skip = True
+
         else:
             msg = "Not skipping %s step (skippable: %s, skip: %s, skipsteps: %s, module_only: %s, force: %s, "
-            msg += "sanity_check_only: %s)"
-            self.log.debug(msg, step, skippable, self.skip, skipsteps, module_only, force, sanity_check_only)
+            msg += "sanity_check_only: %s, skip_extensions: %s, skip_test_step: %s)"
+            self.log.debug(msg, step, skippable, self.skip, skipsteps, module_only, force,
+                           sanity_check_only, skip_extensions, skip_test_step)
 
         return skip
 
@@ -3586,10 +3600,6 @@ def build_and_install_one(ecdict, init_env):
         _log.debug("Skip set to %s" % skip)
         app.cfg['skip'] = skip
 
-    if build_option('skip_test_step'):
-        _log.debug('Adding test_step to skipped steps')
-        app.cfg.update('skipsteps', TEST_STEP, allow_duplicate=False)
-
     # build easyconfig
     errormsg = '(no error)'
     # timing info
diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py
--- a/easybuild/tools/config.py
+++ b/easybuild/tools/config.py
@@ -263,6 +263,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
         'search_paths',
         'sequential',
         'set_gid_bit',
+        'skip_extensions',
         'skip_test_cases',
         'skip_test_step',
         'generate_devel_module',
diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py
--- a/easybuild/tools/options.py
+++ b/easybuild/tools/options.py
@@ -466,6 +466,7 @@ def override_options(self):
             'set-gid-bit': ("Set group ID bit on newly created directories", None, 'store_true', False),
             'silence-deprecation-warnings': ("Silence specified deprecation warnings", 'strlist', 'extend', None),
             'sticky-bit': ("Set sticky bit on newly created directories", None, 'store_true', False),
+            'skip-extensions': ("Skip installation of extensions", None, 'store_true', False),
             'skip-test-cases': ("Skip running test cases", None, 'store_true', False, 't'),
             'skip-test-step': ("Skip running the test step (e.g. unit tests)", None, 'store_true', False),
             'generate-devel-module': ("Generate a develop module file, implies --force if disabled",


Test patch:
diff --git a/test/framework/options.py b/test/framework/options.py
--- a/test/framework/options.py
+++ b/test/framework/options.py
@@ -5857,6 +5857,42 @@ def test_sanity_check_only(self):
             regex = re.compile(error_pattern)
             self.assertTrue(regex.search(error_msg), "Pattern '%s' should be found in: %s" % (regex.pattern, error_msg))
 
+        # failing sanity check for extension can be bypassed via --skip-extensions
+        outtxt = self.eb_main(args + ['--skip-extensions'], do_build=True, raise_error=True)
+        self.assertTrue("Sanity check for toy successful" in outtxt)
+
+    def test_skip_extensions(self):
+        """Test use of --skip-extensions."""
+        topdir = os.path.abspath(os.path.dirname(__file__))
+        toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+
+        # add extension, which should be skipped
+        test_ec = os.path.join(self.test_prefix, 'test.ec')
+        test_ec_txt = read_file(toy_ec)
+        test_ec_txt += '\n' + '\n'.join([
+            "exts_list = [",
+            "    ('barbar', '0.0', {",
+            "        'start_dir': 'src',",
+            "        'exts_filter': ('ls -l lib/lib%(ext_name)s.a', ''),",
+            "    })",
+            "]",
+        ])
+        write_file(test_ec, test_ec_txt)
+
+        args = [test_ec, '--force', '--skip-extensions']
+        self.eb_main(args, do_build=True, return_error=True)
+
+        toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
+        if get_module_syntax() == 'Lua':
+            toy_mod += '.lua'
+
+        self.assertTrue(os.path.exists(toy_mod), "%s should exist" % toy_mod)
+
+        toy_installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
+        for path in (os.path.join('bin', 'barbar'), os.path.join('lib', 'libbarbar.a')):
+            path = os.path.join(toy_installdir, path)
+            self.assertFalse(os.path.exists(path), "Path %s should not exist" % path)
+
     def test_fake_vsc_include(self):
         """Test whether fake 'vsc' namespace is triggered for modules included via --include-*."""
 
diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py
--- a/test/framework/toy_build.py
+++ b/test/framework/toy_build.py
@@ -1730,6 +1730,12 @@ def test_module_only_extensions(self):
                                   do_build=True, raise_error=True)
         self.assertFalse(os.path.exists(toy_mod))
 
+        # failing sanity check for barbar extension is ignored when using --module-only --skip-extensions
+        for extra_args in (['--module-only'], ['--module-only', '--rebuild']):
+            self.eb_main([test_ec, '--skip-extensions'] + extra_args, do_build=True, raise_error=True)
+            self.assertTrue(os.path.exists(toy_mod))
+            remove_file(toy_mod)
+
         # we can force module generation via --force (which skips sanity check entirely)
         self.eb_main([test_ec, '--module-only', '--force'], do_build=True, raise_error=True)
         self.assertTrue(os.path.exists(toy_mod))