1###############################################################################
2# Copyright (c) 2013 INRIA
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License version 2 as
6# published by the Free Software Foundation;
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16#
17# Authors: Daniel Camara  <daniel.camara@inria.fr>
18#          Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
19###############################################################################
20'''
21 ModuleSource.py
22
23 This file stores the real source fetching implementations for each one of
24 the handled source code repository tools. It is this class that defines how
25 a download of a zip file, or a mercurial repository will be made.
26'''
27import urllib
28try:
29    from urllib.parse import urlparse
30    from urllib.request import urlretrieve
31except ImportError:
32    from urlparse import urlparse
33    from urllib import urlretrieve
34import bake.Utils
35from bake.Exceptions import TaskError
36from bake.Utils import ModuleAttributeBase
37import os
38import re
39import platform
40try:
41    import distro
42except ImportError:
43    import sys
44    print(">>> Error: missing Python 'distro' package; please install")
45    sys.exit(1)
46import subprocess
47import importlib
48try:
49    import commands
50    from commands import getoutput
51except ImportError:
52    from subprocess import getoutput
53from datetime import date
54
55class ModuleSource(ModuleAttributeBase):
56    """ Generic Source class, stores the generic methods for the source
57    code repository tools.
58    """
59
60    def __init__(self):
61        """ Generic attributes definition."""
62
63        ModuleAttributeBase.__init__(self)
64        self.add_attribute('module_directory', '', 'Source module directory',
65                           mandatory=False)
66        self.add_attribute('patch', '', 'code to patch after download',
67                           mandatory=False)
68
69        self.add_attribute('new_variable', '', 'Appends the value to the'
70                           ' system variable on the format VARIABLE1=value1'
71                           ';VARIABLE2=value2', mandatory=False)
72
73        self.add_attribute('post_download', '', 'UNIX Command to run'
74                           ' after the download', mandatory=False)
75
76    @classmethod
77    def subclasses(self):
78        return ModuleSource.__subclasses__()
79
80    @classmethod
81    def create(cls, name):
82        """Instantiates the class that is called by the requested name."""
83
84        # Runs over all the Classes and instantiates the one that has the name
85        # equals to the name passed as parameter
86        for subclass in ModuleSource.subclasses():
87            if subclass.name() == name:
88                return subclass()
89        return None
90
91    # Methods to be implemented by the inherited classes
92    def diff(self, env):
93        raise NotImplemented()
94    def download(self, env):
95        raise NotImplemented()
96    def update(self, env):
97        raise NotImplemented()
98    def check_version(self, env):
99        raise NotImplemented()
100
101    def perform_post_download(self, env):
102        """ Executes a list of Linux commands AFTER the download is finished """
103
104        if self.attribute('post_download').value != '':
105            try:
106                env._logger.commands.write(" > " + env.replace_variables(self.attribute('post_download').value))
107                var = getoutput(env.replace_variables(self.attribute('post_download').value))
108
109                if env.debug:
110                    print("  -> " +  var)
111            except Exception as e:
112                print ("   > Error executing post download : " + e )
113
114
115    @classmethod
116    def source_systemtool(self):
117        """Returns the name of the system instalation tool for this machine."""
118        tools = dict()
119        tools['debian']= 'apt-get'
120        tools['ubuntu']= 'apt-get'
121        tools['linuxmint']= 'apt-get'
122        tools['fedora']= 'yum'
123        tools['redhat']= 'yum'
124        tools['centos']= 'yum'
125        tools['suse']= 'yast'
126        tools['darwin']= 'port'
127
128        osName = platform.system().lower()
129        if osName.startswith('linux'):
130            (distribution, version, version_id) = distro.linux_distribution()
131            if not distribution:
132                distribution = osName
133            else:
134                distribution = distribution.lower()
135        else:
136            distribution = osName
137
138        if distribution in tools:
139            return tools[distribution]
140        else :
141            return ''
142
143    def _check_dependency_expression(self, env, valueToTest):
144        """Verifies if the preconditions exist or not."""
145
146        # if there is no test to do, return true
147        if(not valueToTest):
148            return True
149
150        stringToChange = valueToTest
151
152        ### Clean expression
153        # elements to ignore
154        lib_var = [r'\b(or)\b', r'\b(not)\b', r'\b(and)\b',r'\b(if)\b']
155
156        stringToChange = re.sub(r'(\(|\))',' ',stringToChange)
157        for element in lib_var :
158            stringToChange = re.sub(element,'',stringToChange)
159
160        stringToChange = re.sub(' +',' ', stringToChange)
161
162        # split the command names
163        if re.search(' ', stringToChange):
164            elementsToChange = stringToChange.split()
165        else :
166            elementsToChange = [stringToChange]
167
168        # add the call to the function that verifies if the program exist
169        elementsSet = set([])
170        for element in elementsToChange:
171            elementsSet.add(element)
172
173
174        stringToChange = self._add_command_calls(valueToTest.replace('\\',''),
175                                                 elementsSet)
176
177        # Evaluate if all the programs exist
178        returnValue = eval(stringToChange)
179        return returnValue
180
181    def _split_path_expression(self, inputString):
182        """Split and clean the path expression"""
183        if not inputString:
184            return set([])
185
186        ### Clean expression
187        # elements to ignore
188        # XXX the 'not', 'and', and 'if' separators are being treated
189        # as 'or'; also, there should be some string checking for validity
190        lib_var = [r'\b(or)\b', r'\b(not)\b', r'\b(and)\b',r'\b(if)\b']
191
192        inputString = re.sub(r'(\(|\))',' ',inputString)
193        for element in lib_var :
194            inputString = re.sub(element,'',inputString)
195
196        inputString = re.sub(' +',' ', inputString)
197
198        # split the command names
199        if re.search(' ', inputString):
200            elementsToChange = inputString.split()
201        else :
202            elementsToChange = [inputString]
203
204        # add the call to the function that verifies if the program exist
205        elementsSet = set([])
206        for element in elementsToChange:
207            elementsSet.add(element)
208
209        return elementsSet
210
211    def _check_file_expression(self, valueToTest):
212        """Verifies if the system has the requested file or symbolic link"""
213
214        returnValue = False
215        # if there is no test to do, return true
216        if(not valueToTest):
217            return True
218
219        # The expression may contain a list of 'or' separated paths to check
220        # Split them into a set
221        elementsSet = self._split_path_expression (valueToTest)
222
223        for e in elementsSet:
224           if (os.path.isfile (e) or os.path.islink(e)):
225               returnValue = True
226               break
227        return returnValue
228
229    def _check_executable_expression(self, valueToTest):
230        """Verifies if the system has the requested executable"""
231
232        returnValue = False
233        # if there is no test to do, return true
234        if(not valueToTest):
235            return True
236
237        # The expression may contain a list of 'or' separated paths to check
238        # Split them into a set
239        elementsSet = self._split_path_expression (valueToTest)
240
241        for e in elementsSet:
242           if (os.path.exists (e) and os.access (e, os.X_OK)):
243               returnValue = True
244               break
245        return returnValue
246
247    def _check_import(self, valueToTest):
248        """Verifies if the system has the requested python import"""
249
250        try:
251            importlib.import_module(valueToTest, package=None)
252        except ImportError:
253            return False
254        return True
255
256class NoneModuleSource(ModuleSource):
257    """ This class defines an empty source, i.e. no source code fetching is
258    needed. For compatibility purposes, it is possible to create a given module
259    has no need for source code fetching.
260    """
261
262    def __init__(self):
263        ModuleSource.__init__(self)
264    @classmethod
265    def name(cls):
266        return 'none'
267    def diff(self, env):
268        pass
269    def download(self, env):
270        pass
271    def update(self, env):
272        pass
273    def check_version(self, env):
274        return True
275
276
277class InlineModuleSource(ModuleSource):
278    """ This class enables one to create a python function, using the Bake
279    framework, to  directly to search for code.
280    """
281
282    def __init__(self):
283        ModuleSource.__init__(self)
284
285    @classmethod
286    def name(cls):
287        return 'inline'
288
289
290class BazaarModuleSource(ModuleSource):
291    """Handles the modules that have the sources stored in a bazaar repository."""
292
293    def __init__(self):
294        """ Specific attributes definition."""
295
296        ModuleSource.__init__(self)
297        self.add_attribute('url', '', 'The url to clone from',
298                           mandatory=True)
299        self.add_attribute('revision', None, 'The revision to update to after'
300                           ' the clone.')
301    @classmethod
302    def name(cls):
303        """ Identifier of the type of the tool used."""
304        return 'bazaar'
305
306    def diff(self, env):
307        pass
308
309    def download(self, env):
310        """ Downloads the code, of a specific version, using Bazaar."""
311
312        rev_arg = []
313        if not self.attribute('revision').value is None:
314            rev_arg.extend(['-r', self.attribute('revision').value])
315        env.run(['bzr', 'branch'] + rev_arg + [self.attribute('url').value,
316                                              env.srcdir])
317
318    def update(self, env):
319        """ Updates the code using a specific version from the repository."""
320
321        rev_arg = []
322        if not self.attribute('revision').value is None:
323            rev_arg.extend(['-r', self.attribute('revision').value])
324        env.run(['bzr', 'pull'] + rev_arg + [self.attribute('url').value],
325                directory=env.srcdir)
326
327    def check_version(self, env):
328        """ Checks if the tool is available and with the needed version."""
329        return env.check_program('bzr', version_arg='--version',
330                                 version_regexp='(\d+)\.(\d+)',
331                                 version_required=(2, 1))
332
333
334class MercurialModuleSource(ModuleSource):
335    """Handles the modules that have the sources stored in a mercurial
336    repository.
337    """
338
339    def __init__(self):
340        """ Specific attributes definition."""
341
342        ModuleSource.__init__(self)
343        self.add_attribute('url', '', 'The url to clone from',
344                            mandatory=True)
345        self.add_attribute('revision', 'tip', 'The revision to update to '
346                           'after the clone. '
347                           'If no value is specified, the default is "tip"')
348    @classmethod
349    def name(cls):
350        """ Identifier of the type of the tool used."""
351        return 'mercurial'
352
353    def download(self, env):
354        """ Downloads the code, of a specific version, using Mercurial."""
355
356        env.run(['hg', 'clone', '-U', self.attribute('url').value, env.srcdir])
357        env.run(['hg', 'update', '-r', self.attribute('revision').value],
358                    directory=env.srcdir)
359
360    def update(self, env):
361        """ Updates the code using a specific version from the repository."""
362
363        env.run(['hg', 'pull', self.attribute('url').value],
364                directory=env.srcdir)
365        env.run(['hg', 'update', '-r', self.attribute('revision').value],
366                directory=env.srcdir)
367
368    def check_version(self, env):
369        """ Checks if the tool is available and with the needed version."""
370        return env.check_program('hg')
371
372
373import shutil
374class ArchiveModuleSource(ModuleSource):
375    """Handles the modules that have the sources as a single tarball like file."""
376
377    def __init__(self):
378        """ Specific attributes definition."""
379
380        ModuleSource.__init__(self)
381        self.add_attribute('url', None, 'The url to clone from',
382                           mandatory=True)
383        self.add_attribute('additional-module', None,
384                           "Tags this module as an additional sub-module to be"
385                           " added to another module.",
386                           mandatory=False)
387        self.add_attribute('extract_directory', None,
388                           "The name of the directory the source code will "
389                           "be extracted to naturally. If no value is "
390                           "specified, directory is assumed to be equal to "
391                           "the  archive without the file extension.")
392    @classmethod
393    def name(cls):
394        """ Identifier of the type of the tool used."""
395        return 'archive'
396
397    def _decompress(self, filename, env):
398        """Uses the appropriate tool to uncompress the sources."""
399
400        import tempfile
401        import os
402        tempdir = tempfile.mkdtemp(dir=env.srcrepo)
403        extensions = [
404            ['tar', ['tar', 'xf']],
405            ['tar.gz', ['tar', 'zxf']],
406            ['tar.Z', ['tar', 'zxf']],
407            ['tar.bz2', ['tar', 'jxf']],
408            ['zip', ['unzip']],
409            ['rar', ['unrar', 'e']],
410            ['tar.xz', ['tar', 'Jxf']],
411            ['xz', ['unxz']],
412            ['7z', ['7z', 'x']],
413            ['tgz', ['tar', 'xzvf']],
414            ['tbz2', ['tar', 'jxf']]
415           ]
416
417        # finds the right tool
418        for extension, command in extensions:
419            if filename.endswith(extension):
420                env.run(command + [filename], directory=tempdir)
421                if self.attribute('extract_directory').value is not None:
422                    actual_extract_dir = self.attribute('extract_directory').value
423                else:
424                    actual_extract_dir = os.path.basename(filename)[0:-len(extension) - 1]
425
426                # finally, rename the extraction directory to the target
427                # directory name.
428                try:
429                    destDir=os.path.join(tempdir, actual_extract_dir)
430
431                    if os.path.isdir(env.srcdir):
432                        bake.Utils.mergeDirs(destDir, env.srcdir)
433                    else:
434                        os.rename(destDir, env.srcdir)
435
436                    shutil.rmtree(tempdir) # delete directory
437                except (OSError, IOError) as e:
438                    raise TaskError("Rename problem for module: %s, from: %s, "
439                                    "to: %s, Error: %s"
440                                    % (env._module_name,os.path.join(tempdir,
441                                                                     actual_extract_dir),
442                                       env.srcdir, e))
443                return
444        raise TaskError('Unknown Archive Type: %s, for module: %s' %
445                        (filename, env._module_name))
446
447    def download(self, env):
448        """Downloads the specific file."""
449
450
451
452        url_local = self.attribute('url').value
453
454        filename = os.path.basename(urlparse(url_local).path)
455        tmpfile = os.path.join(env.srcrepo, filename)
456        try:
457            urlretrieve(url_local, filename=tmpfile)
458        except IOError as e:
459            raise TaskError('Download problem for module: %s, URL: %s, Error: %s'
460                            % (env._module_name,self.attribute('url').value, e))
461
462        self._decompress(tmpfile, env)
463
464    def update(self, env):
465        """ Empty, no update is possible for files."""
466        pass
467
468    def check_version(self, env):
469
470        """Verifies if the right program exists in the system to handle the
471         given compressed source file.
472         """
473        extensions = [
474            ['tar', 'tar'],
475            ['tar.gz', 'tar'],
476            ['tar.Z', 'tar'],
477            ['tar.bz2', 'tar'],
478            ['tgz', 'tar'],
479            ['zip', 'unzip'],
480            ['rar', 'unrar'],
481            ['7z', '7z'],
482            ['xz', 'unxz'],
483            ['Z', 'uncompress']
484            ]
485        try:
486            filename = os.path.basename(urlparse(self.attribute('url').value).path)
487        except AttributeError as e:
488            return False
489
490        for extension, program in extensions:
491            if filename.endswith(extension):
492                return env.check_program(program)
493        return False
494
495
496class SystemDependency(ModuleSource):
497    """Handles the system dependencies for a given module.  If a system
498       dependency is not met, advise the user on how to install the
499       missing dependency, if possible.  Dependencies may be expressed
500       by requesting bake to check for a specific file (such as a library
501       or header file) in one or more locations, or checking for an
502       executable program in one or more locations.
503    """
504
505    def __init__(self):
506        """ Specific attributes definition."""
507
508        ModuleSource.__init__(self)
509        self.add_attribute('dependency_test', None,
510                           '(DEPRECATED) The name of the dependency',
511                           mandatory=False)
512        self.add_attribute('file_test', None, 'System file to try to locate',
513                           mandatory=False)
514        self.add_attribute('executable_test', None, 'Executable to try to locate',
515                           mandatory=False)
516        self.add_attribute('import_test', None, 'Python import to try',
517                           mandatory=False)
518        self.add_attribute('try_to_install', 'false',
519                           '(DEPRECATED) If should try to install or not',
520                           mandatory=False)
521        self.add_attribute('sudoer_install', None,
522                           '(DEPRECATED) Try to install the dependency as sudoer',
523                           mandatory=False)
524        self.add_attribute('name_yum', None,
525                           'The package name of the module, for RPMs',
526                           mandatory=False)
527        self.add_attribute('name_apt-get', None,
528                           'The package name of the module, for use with apt-get ',
529                           mandatory=False)
530        self.add_attribute('name_yast', None,
531                           'The package name of the module, for use with yast',
532                           mandatory=False)
533        self.add_attribute('name_port', None,
534                           'The package name of the module, for use with port (Mac OS)',
535                           mandatory=False)
536        self.add_attribute('more_information', None,
537                           'Gives users a better hint of where to search for the module' ,
538                           mandatory=True)
539    @classmethod
540    def name(cls):
541        """ Identifier of the type of the tool used."""
542        return 'system_dependency'
543
544    def _get_command(self, distribution):
545        """Finds the proper installer command line, given the OS distribution.
546        """
547
548        distributions = [
549            ['debian', 'apt-get install '],
550            ['ubuntu', 'apt-get install '],
551            ['linuxmint', 'apt-get install '],
552            ['fedora', 'yum install '],
553            ['redhat', 'yum install '],
554            ['centos', 'yum install '],
555            ['suse', 'yast --install '],
556            ['darwin', 'port install '],
557            ]
558
559        for name, command in distributions:
560            if distribution.startswith(name):
561                return command
562        return ''
563
564    def remove(self, env):
565        """ Removes the the present version of the dependency."""
566
567        # if the download is dependent of the machine's architecture
568        osName = platform.system().lower()
569        if(not osName.startswith('linux') and not osName.startswith('darwin')):
570            raise TaskError("Not a Linux/Mac OS machine, self installation is not"
571                            " possible in %s for module: %s,  %s" %
572                            (osName, env._module_name,
573                             self.attribute('error_message').value))
574
575        (distribution, version, version_id) = distro.linux_distribution()
576
577        if not distribution:
578            distribution = 'darwin' # osName
579        else:
580            distribution = distribution.lower()
581
582        command = self._get_command(distribution)
583        command = command.rstrip().rsplit(' ', 1)[0] + ' remove'
584        installerName = self.attribute('name_' + command.split()[0]).value
585
586        # if didn't find the specific installer name uses the default one
587        if(not installerName):
588            installerName = env._module_name
589
590        # if should try to remove as sudoer
591        sudoer=self.attribute('sudoer_install').value
592        if sudoer: sudoer = sudoer.lower()
593        if(sudoer =='true' and (not env.sudoEnabled)):
594            raise TaskError('    Module: \"%s\" requires sudo rights, ask your'
595                            ' system admin to remove \"%s\" from your machine.\n'
596                            '    More information from the module: \"%s\"'
597                            % (env._module_name, installerName,
598                               self.attribute('more_information').value))
599
600        if(env.sudoEnabled):
601            command = "sudo "+ command
602            command = command
603
604        # uses apt-get/yum/... to remove the module
605        try:
606            env.run((command + " " + installerName).split(" "),
607                    directory=env.srcrepo)
608        except IOError as e:
609            raise TaskError('    Removing module problem: \"%s\", Message: %s, Error: %s'
610                            % (env._module_name,
611                               self.attribute('more_information').value, e))
612        except TaskError as e1:
613            if(env.sudoEnabled):
614                e1.reason = ("    Removing problem for module: \"%s\", "
615                            "\n    Probably either you miss rights or the module is"
616                            " not present on your package management databases."
617                            "\n    Try to either talk to your system admin or review your "
618                            "library database to add \"%s\"\n"
619                            "    More information from the module: \"%s\""
620                            % (env._module_name, installerName,
621                               self.attribute('more_information').value))
622            else:
623                e1.reason = ("    Removing problem for module: \"%s\", "
624                            "\n    Probably you either need super user rights"
625                            " to remove the packet, or the module is"
626                            " not present on your package management databases."
627                            "\n    Try calling bake with the --sudo option and/or "
628                            "review your library database to add \"%s\"\n"
629                                "    More information from the module: \"%s\""
630                            % (env._module_name, installerName,
631                               self.attribute('more_information').value))
632
633            raise TaskError("    Removing module problem: \"%s\",\n    Probably you"
634            "miss sudo rights or the module is not present on your package "
635            "management databases. \n    Try calling bake with --sudo or reviewing your"
636                            " library database to add \"%s\"\n"
637                                "    More information from the module: \"%s\""
638                            % (env._module_name, installerName,
639                               self.attribute('more_information').value))
640        return True
641
642    def _add_command_calls(self, stringToChange, elements):
643        """ Define the command calls to be executed. """
644
645        for element in elements:
646            stringToChange= re.sub(element + "(\s|\)|$)" ,
647                                   'env.check_program(\'' + element.replace('\\','') +
648                                   '\')\\1', stringToChange)
649        return stringToChange
650
651
652
653    def download(self, env):
654        """ Verifies if the system dependency exists, if exists returns true,
655        if not, and we are in a supported machine, tries to download and install
656        the dependency.
657        """
658
659        selfInstalation = self.attribute('try_to_install').value
660        if selfInstalation: selfInstalation = selfInstalation.lower()
661
662        if not selfInstalation == 'true' :
663            raise TaskError('    Module: \"%s\" is required by other modules but it is not available on your system.\n'
664                    '     Ask your system admin or review your library database to add \"%s\"\n'
665                    '    More information from the module: \"%s\"'
666                            % (env._module_name, env._module_name,
667                               self.attribute('more_information').value))
668
669        # even if should try to install, if it is not a supported machine
670        # we will not be able to
671        osName = platform.system().lower().strip()
672        if((osName.startswith('linux') or osName.startswith('darwin')) and
673           selfInstalation == 'true'):
674            (distribution, version, version_id) = distro.linux_distribution()
675
676            if not distribution:
677                distribution = osName.split()[0] # osName
678            else:
679                distribution = distribution.lower()
680
681            command = self._get_command(distribution)
682
683            # didn't recognize the distribution, asks user to install by himself
684            if command == '' :
685                raise TaskError('    Module: \"%s\" is required by other modules but it is not available on your system.\n'
686                    '    Ask your system admin\n'
687                    '    > More information from the module: \"%s\"'
688                            % (env._module_name,
689                               self.attribute('more_information').value))
690
691            installerName = self.attribute('name_' + command.split()[0]).value
692
693            # if didn't find the specific installer name uses the default one
694            if(not installerName):
695                installerName = env._module_name
696
697            if(not command):
698                selfInstalation = 'false'
699
700        else :
701            selfInstalation = 'false'
702
703        if not env._sudoEnabled :
704            raise TaskError('    Module: \"%s\" is required by other modules and is not available on your system.\n'
705                            '    Ask your system admin to install it.\n'
706                            '    > More information from the module: \"%s\"'
707                            % (env._module_name,
708                               self.attribute('more_information').value))
709
710
711        errorTmp = None
712        sudoer=self.attribute('sudoer_install').value
713        if selfInstalation=='true':
714            # Try to install if possible
715
716            # if should try to install as sudoer
717            if sudoer: sudoer = sudoer.lower()
718            if(sudoer=='true' and (not env.sudoEnabled)):
719                raise TaskError('   Module: \"%s\" requires sudo rights, if'
720                                ' you have the right, call bake with the'
721                                ' --sudo option, or ask your system admin'
722                                ' to install \"%s\" in your machine.\n'
723                                '    > More information from the module: \"%s\"'
724                            % (env._module_name, installerName,
725                               self.attribute('more_information').value))
726
727            # if the user asked to install everything as sudoer... do it!
728            if(env.sudoEnabled):
729                command = "sudo "+ command
730                command = command
731
732            try:
733                env.run((command + installerName).split(" "),
734                        directory=env.srcrepo)
735                return True
736            except IOError as e:
737                errorTmp = ('    Self installation problem for module: \"%s\", '
738                            'Error: %s' % (env._module_name,  e))
739            except TaskError as e1:
740                if(env.sudoEnabled):
741                    e1.reason = ("    Self installation problem for module: \"%s\", "
742                            "\n    Probably either you miss sudo rights or the module is"
743                            " not present on your package management databases."
744                            "\n    Try to either talk to your system admin or review your "
745                            "library database to add \"%s\"\n"
746                            "    > More information from the module: \"%s\""
747                            % (env._module_name, installerName,
748                               self.attribute('more_information').value))
749                else:
750                    e1.reason = ("    Self installation problem for module: \"%s\", "
751                            "\n    Probably either you need super user rights to install the packet,"
752                            "or that the module is"
753                            " not present on your package management databases."
754                            "\n    Try calling bake with the --sudo option and/or "
755                            "review your library database to add \"%s\"\n"
756                            "    > More information from the module: \"%s\""
757                            % (env._module_name, installerName,
758                               self.attribute('more_information').value))
759                raise e1
760
761        return True
762
763    def update(self, env):
764        """Empty, no Update available for system dependency. """
765        pass
766
767    def build(self, env):
768        """ Empty, no build is possible for system dependencies."""
769        pass
770
771    def check_version(self, env):
772        """Verifies if the right program exists in the system to handle
773        the given compressed source file.
774        """
775#
776#        distributions = [
777#            ['debian', 'apt-get'],
778#            ['ubuntu', 'apt-get'],
779#            ['fedora', 'yum'],
780#            ['redhat', 'yum'],
781#            ['centos', 'yum'],
782#            ['suse', 'yast'],
783#            ['darwin', 'port'],
784#            ]
785#
786
787#        (distribution, version, version_id) = distro.linux_distribution()
788#        if not distribution:
789#            distribution = 'darwin' # osName
790#        else:
791#            distribution = distribution.lower()
792
793        program = self.source_systemtool()
794
795        if not program == '':
796            return env.check_program(program)
797#        for dist, program in distributions:
798#            if distribution.startswith(dist):
799#                return env.check_program(program)
800        return False
801
802
803class CvsModuleSource(ModuleSource):
804    """Handles the modules that have the sources stored in a CVS repository."""
805
806    def __init__(self):
807        """ Specific attributes definition."""
808
809        ModuleSource.__init__(self)
810        self.add_attribute('root', '',
811                           'Repository root specification to checkout from.',
812                           mandatory=True)
813        self.add_attribute('module', '', 'Module to checkout.', mandatory=True)
814        self.add_attribute('checkout_directory', None, "Name of directory "
815                           "checkout defaults to. If unspecified, defaults"
816                           " to the name of the module being checked out.")
817        self.add_attribute('date', None, 'Date to checkout')
818
819    @classmethod
820    def name(cls):
821        """ Identifier of the type of the tool used."""
822
823        return 'cvs'
824
825    def download(self, env):
826        """ Downloads the last CVS code, or from a specific date. """
827
828        import tempfile
829        try:
830            tempdir = tempfile.mkdtemp(dir=env.srcrepo)
831        except OSError as e:
832            raise TaskError('Could not create temporary directory %s, Error: %s'
833                            % (env.srcrepo, e))
834
835        env.run(['cvs', '-d', self.attribute('root').value, 'login'],
836                directory=tempdir)
837
838        checkout_options = []
839        if not self.attribute('date').value is None:
840            checkout_options.extend(['-D', self.attribute('date').value])
841        env.run(['cvs', '-d', self.attribute('root').value, 'checkout'] +
842                checkout_options + [self.attribute('module').value],
843                directory=tempdir)
844
845        if self.attribute('checkout_directory').value is not None:
846            actual_checkout_dir = self.attribute('checkout_directory').value
847        else:
848            actual_checkout_dir = self.attribute('module').value
849
850        import os
851        import shutil
852        try:
853            os.rename(os.path.join(tempdir, actual_checkout_dir), env.srcdir)
854            shutil.rmtree(tempdir)
855        except AttributeError as e:
856            raise TaskError('Atribute type error expected String, Error: %s'
857                            % e)
858
859
860    def update(self, env):
861        """Updates the code for the date specified, or for the today's code. """
862
863        # just update does not work, it has to give a date for the update
864        # either a date is provided, or takes today as date
865        update_options = []
866        if not self.attribute('date').value is None:
867            update_options.extend(['-D', self.attribute('date').value])
868        else:
869            update_options.extend(['-D', str(date.today())])
870
871        env.run(['cvs', 'up'] + update_options, directory=env.srcdir)
872
873    def check_version(self, env):
874        """ Checks if the tool is available and with the needed version."""
875
876        return env.check_program('cvs')
877
878
879class GitModuleSource(ModuleSource):
880    """Handles the modules that have the sources stored in a git repository."""
881
882    def __init__(self):
883        ModuleSource.__init__(self)
884        self.add_attribute('url', '', 'Url to clone the source tree from.',
885                           mandatory=True)
886        self.add_attribute('revision', '',
887                           "Revision to checkout. Defaults to origin/master"
888                           " reference.")
889        self.add_attribute('branch', '', 'Branch to checkout.')
890        self.add_attribute('fetch_option', '', 'Options to add git fetch command.')
891    @classmethod
892    def name(cls):
893        """ Identifier of the type of the tool used."""
894
895        return 'git'
896
897    def download(self, env):
898        import tempfile
899        import os
900        try:
901            tempdir = tempfile.mkdtemp(dir=env.srcrepo)
902        except AttributeError as e1:
903            raise TaskError('Attribute type error, expected String, Error: %s' % e1)
904        except OSError as e2:
905            raise TaskError('Could not create temporary file, Error: %s' % e2)
906
907        checkedOut = False
908
909        env.run(['git', 'init'], directory=tempdir)
910        env.run(['git', 'remote', 'add', 'origin', self.attribute('url').value],
911                directory=tempdir)
912        if self.attribute('fetch_option').value != '':
913            env.run(['git', 'fetch', self.attribute('fetch_option').value],
914                    directory=tempdir)
915        else:
916            env.run(['git', 'fetch'], directory=tempdir)
917
918        if self.attribute('branch').value != '':
919            env.run(['git', 'checkout', self.attribute('branch').value],
920                    directory=tempdir)
921            checkedOut = True
922
923        if self.attribute('revision').value != '':
924            env.run(['git', 'checkout', self.attribute('revision').value],
925                    directory=tempdir)
926            checkedOut = True
927
928        if not checkedOut:
929            env.run(['git', 'pull', 'origin', 'master'], directory=tempdir)
930
931        os.rename(tempdir, env.srcdir)
932
933    def update(self, env):
934        """ Updates the code using a specific version from the repository."""
935
936        env.run(['git', 'stash'], directory=env.srcdir)
937        env.run(['git', 'rebase', self.attribute('revision').value], directory=env.srcdir)
938        try:
939            env.run(['git', 'stash','pop'], directory=env.srcdir)
940        except TaskError as t:
941            if not ' 1' in t.reason:
942                raise t
943            env._logger.commands.write('  No perceived changes on the local repository.\n')
944
945
946#        env.run(['git', 'fetch'], directory=env.srcdir)
947#        env.run(['git', 'checkout', self.attribute('revision').value],
948#                          directory=env.srcdir)
949
950    def check_version(self, env):
951        """ Checks if the tool is available and with the needed version."""
952        return env.check_program('git')
953