Repository: easybuilders/easybuild-framework

Issue:
EasyBuild fails to parse eb file stored in archive
EasyBuild fails to parse the following eb file stored in the archive for GCC/4.9.0 built on our test cluster (running Debian 7 "wheezy"):

```
# Built with EasyBuild version 1.14.0dev on 2014-06-29_11-37-43
name = "GCC"
version = '4.9.0'
versionsuffix = "" # tweaked by EasyBuild (was: "-CLooG")

homepage = 'http://gcc.gnu.org/'
description = """The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Java, and Ada,
 as well as libraries for these languages (libstdc++, libgcj,...)."""

toolchain = {'name': 'dummy', 'version': 'dummy'}

source_urls = [
    'http://ftpmirror.gnu.org/%(namelower)s/%(namelower)s-%(version)s',  # GCC auto-resolving HTTP mirror
    'http://ftpmirror.gnu.org/gmp',  # idem for GMP
    'http://ftpmirror.gnu.org/mpfr',  # idem for MPFR
    'http://www.multiprecision.org/mpc/download',  # MPC official
    'http://www.bastoul.net/cloog/pages/download/',  # CLooG official
    'ftp://gcc.gnu.org/pub/gcc/infrastructure/',  # GCC dependencies
    'http://gcc.cybermirror.org/infrastructure/',  # HTTP mirror for GCC dependencies
]

mpfr_version = '3.1.2'

sources = [
    SOURCELOWER_TAR_BZ2,
    'gmp-6.0.0a.tar.bz2',
    'mpfr-%s.tar.gz' % mpfr_version,
    'mpc-1.0.2.tar.gz',
    'cloog-0.18.1.tar.gz',
    'isl-0.12.2.tar.bz2',
]

patches = [('mpfr-%s-allpatches-20140416.patch' % mpfr_version, '../mpfr-%s' % mpfr_version)]

languages = ['c', 'c++', 'fortran', 'lto']

withcloog = True
withisl = True
clooguseisl = True

# building GCC sometimes fails if make parallelism is too high, so let's limit it
maxparallel = "" # tweaked by EasyBuild (was: 4)

moduleclass = 'compiler'

# Build statistics
buildstats = [{
    'easybuild-framework_version': '1.14.0dev',
    'easybuild-easyblocks_version': '1.14.0dev',
    'timestamp': 1404034663,
    'build_time': 1295.44,
    'install_size': 623235221,
    'command_line': ['--buildpath=/tmp/ebuild', '--debug', '--installpath=/home/geimer/Projects/easybuild3/opt', '--logtostdout', '--module-naming-scheme=SwatModuleNamingScheme', '--modules-tool=Lmod', '--prefix=/home/geimer/Projects/easybuild3/opt', '--repositorypath=/home/geimer/Projects/easybuild3/ebfiles', '--robot=/home/geimer/Projects/eb-develop/easybuild-easyconfigs/easybuild/easyconfigs', '--sourcepath=/home/geimer/Projects/easybuild3/Sources', '--try-amend=maxparallel=', '--try-amend=versionsuffix=', 'GCC-4.9.0-CLooG.eb'],
    'modules_tool': ('Lmod', '/home/geimer/Projects/easybuild3/opt/apps/lmod/lmod/libexec/lmod', '5.6.3'),
    'core_count': 24,
    'cpu_model': 'Intel(R) Xeon(R) CPU           X5660  @ 2.80GHz',
    'cpu_speed': 2800.0,
    'cpu_vendor': 'Intel',
    'gcc_version': '''Using built-in specs.; COLLECT_GCC=gcc; COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.7/lto-wrapper; Target: x86_64-linux-gnu; Configured with: ../src/configure -v --with-pkgversion='Debian 4.7.2-5' --with-bugurl=file:///usr/share/doc/gcc-4.7/README.Bugs --enable-languages=c,c++,go,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.7 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.7 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --with-arch-32=i586 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu; Thread model: posix; gcc version 4.7.2 (Debian 4.7.2-5) ; ''',
    'glibc_version': '2.13',
    'hostname': 'swatc01',
    'kernel_name': 'Linux',
    'os_name': 'debian',
    'os_type': 'Linux',
    'os_version': '7.5',
    'platform_name': 'x86_64-unknown-linux',
    'python_version': '2.7.3 (default, Mar 13 2014, 11:03:55) ; [GCC 4.7.2]',
    'system_gcc_path': '/usr/bin/gcc',
    'system_python_path': '/usr/bin/python',
}]
```

The culprit is the line specifying 'gcc_version' in the build stats, as it contains a command-line option with an argument enclosed in single quotes. This obviously confuses the parser. Manually triple-quoting the full 'gcc_version' string solved the issue, i.e., fixing this bug is "only" a matter of proper quoting.



Discussion:

Resolving patch:
diff --git a/easybuild/framework/easyconfig/tools.py b/easybuild/framework/easyconfig/tools.py
--- a/easybuild/framework/easyconfig/tools.py
+++ b/easybuild/framework/easyconfig/tools.py
@@ -74,6 +74,7 @@
 from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version, det_hidden_modname
 from easybuild.tools.modules import modules_tool
 from easybuild.tools.ordereddict import OrderedDict
+from easybuild.tools.utilities import quote_str
 
 _log = fancylogger.getLogger('easyconfig.tools', fname=False)
 
@@ -388,17 +389,8 @@ def stats_to_str(stats):
         _log.error("Can only pretty print build stats in dictionary form, not of type %s" % type(stats))
 
     txt = "{\n"
-
     pref = "    "
-
-    def tostr(x):
-        if isinstance(x, basestring):
-            return "'%s'" % x
-        else:
-            return str(x)
-
     for (k, v) in stats.items():
-        txt += "%s%s: %s,\n" % (pref, tostr(k), tostr(v))
-
+        txt += "%s%s: %s,\n" % (pref, quote_str(k), quote_str(v))
     txt += "}"
     return txt
diff --git a/easybuild/tools/repository/filerepo.py b/easybuild/tools/repository/filerepo.py
--- a/easybuild/tools/repository/filerepo.py
+++ b/easybuild/tools/repository/filerepo.py
@@ -78,7 +78,7 @@ def add_easyconfig(self, cfg, name, version, stats, previous):
         mkdir(full_path, parents=True)
 
         # destination
-        dest = os.path.join(full_path, "%s.eb" % version)
+        dest = os.path.join(full_path, "%s-%s.eb" % (name, version))
 
         txt = "# Built with EasyBuild version %s on %s\n" % (VERBOSE_VERSION, time.strftime("%Y-%m-%d_%H-%M-%S"))
 


Test patch:
diff --git a/test/framework/easyconfig.py b/test/framework/easyconfig.py
--- a/test/framework/easyconfig.py
+++ b/test/framework/easyconfig.py
@@ -952,6 +952,7 @@ def test_filter_deps(self):
         opts = init_config(args=['--filter-deps=zlib,ncurses'])
         self.assertEqual(opts.filter_deps, ['zlib', 'ncurses'])
 
+
 def suite():
     """ returns all the testcases in this module """
     return TestLoader().loadTestsFromTestCase(EasyConfigTest)
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
@@ -41,6 +41,7 @@
 from vsc.utils.fancylogger import setLogLevelDebug, logToScreen
 
 import easybuild.tools.module_naming_scheme  # required to dynamically load test module naming scheme(s)
+from easybuild.framework.easyconfig.easyconfig import EasyConfig
 from easybuild.tools.build_log import EasyBuildError
 from easybuild.tools.filetools import mkdir, read_file, write_file
 from easybuild.tools.modules import modules_tool
@@ -142,6 +143,8 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True
                                   raise_error=raise_error)
         except Exception, err:
             myerr = err
+            if raise_error:
+                raise myerr
 
         if verify:
             self.check_toy(self.test_installpath, outtxt, versionsuffix=versionsuffix)
@@ -173,9 +176,6 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True
                 msg = "Pattern %s found in full test report: %s" % (regex.pattern, test_report_txt)
                 self.assertTrue(regex.search(test_report_txt), msg)
 
-        if raise_error and (myerr is not None):
-            raise myerr
-
         return outtxt
 
     def test_toy_broken(self):
@@ -681,6 +681,21 @@ def test_module_filepath_tweaking(self):
         self.assertTrue(os.path.exists(os.path.join(mod_file_prefix, 't', 'toy', '0.0')))
         self.assertTrue(os.path.islink(os.path.join(mod_file_prefix, 't', 'toy', '0.0')))
 
+    def test_toy_archived_easyconfig(self):
+        """Test archived easyconfig for a succesful build."""
+        repositorypath = os.path.join(self.test_installpath, 'easyconfigs_archive')
+        extra_args = [
+            '--repository=FileRepository',
+            '--repositorypath=%s' % repositorypath,
+        ]
+        self.test_toy_build(raise_error=True, extra_args=extra_args)
+
+        archived_ec = os.path.join(repositorypath, 'toy', 'toy-0.0.eb')
+        self.assertTrue(os.path.exists(archived_ec))
+        ec = EasyConfig(archived_ec)
+        self.assertEqual(ec.name, 'toy')
+        self.assertEqual(ec.version, '0.0')
+
 def suite():
     """ return all the tests in this file """
     return TestLoader().loadTestsFromTestCase(ToyBuildTest)