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 ModuleEnvironment.py
22
23 This file stores the class Module Environment responsible for the interaction
24 between Bake and the execution of third party softwares and the operating
25 system.
26'''
27
28import os
29import subprocess
30import sys
31import platform
32
33from bake.Exceptions import TaskError
34from bake.Utils import ColorTool
35
36class ModuleEnvironment:
37    ''' Main class to interact with the host system to execute the external
38    tools.
39    '''
40    _stopOnError = False
41    _libpaths = set([])
42    _binpaths = set([])
43    _pkgpaths =  set([])
44    _variables =  set([])
45
46    (HIGHER, LOWER, EQUAL) = range(0,3)
47
48    def __init__(self, logger, installdir, sourcedir, objdir, debug=False):
49        ''' Internal variables initialization.'''
50
51        self._logger = logger
52        self._installdir = installdir
53        self._sourcedir = sourcedir
54        self._objdir = objdir
55        self._module_name = None
56        self._module_dir = None
57        self._module_supports_objdir = None
58#         self._libpaths = set([])
59#         self._binpaths = set([])
60#         self._pkgpaths =  set([])
61#         self._variables =  set([])
62        self._debug = debug
63        self._sudoEnabled = False
64
65    def _module_directory(self):
66        ''' Returns the name of the directory of the on use module.'''
67
68        if not self._module_dir :
69            return self._module_name
70        return self._module_dir
71
72    @property
73    def installdir(self):
74        ''' Returns the name of the set installation directory.'''
75
76        return self._installdir
77
78    @property
79    def debug(self):
80        ''' Returns if this execution was set to show the debug messages or not.'''
81
82        return self._debug
83
84    @property
85    def srcdir(self):
86        ''' Returns the directory where Bake stores the source of the present
87        module.
88        '''
89
90        try:
91            return os.path.join(self._sourcedir, self._module_directory())
92        except AttributeError as e:
93            raise TaskError('Missing configuration: sourcedir= %s, '
94                            'module_directory= %s, Error: %s'
95                            % (self._sourcedir,self._module_directory(), e))
96
97    @property
98    def srcrepo(self):
99        ''' The root of the source repository, where all the sources for all
100        the modules will be stored.
101        '''
102
103        return self._sourcedir
104
105    @property
106    def objdir(self):
107        ''' Returns the directory where Bake stores the object code of the
108        present module.
109        '''
110
111        if not self._module_supports_objdir:
112            obj = self.srcdir
113        else:
114            try:
115                obj = os.path.join(self.srcdir, self._objdir)
116            except AttributeError as e:
117                raise TaskError('Missing configuration: sourcedir= %s, '
118                                'objdir= %s, Error: %s'
119                                % (self._sourcedir, self._module_directory(), e))
120        return obj
121
122    @property
123    def sudoEnabled(self):
124        ''' Returns the setting of the --sudo option'''
125
126        return self._sudoEnabled
127
128    @property
129    def stopOnErrorEnabled(self):
130        ''' Returns the setting of the --stop_on_error option'''
131
132        return ModuleEnvironment._stopOnError
133
134    def _pkgconfig_var(self):
135        ''' Returns the PKG_CONFIG_PATH configured environment variable.'''
136
137        return 'PKG_CONFIG_PATH'
138
139    def _pkgconfig_path(self):
140        ''' Returns the PKG_CONFIG_PATH configured path. '''
141
142        return os.path.join(self._lib_path(), 'pkgconfig')
143
144    def _lib_var(self):
145        ''' Returns the value of the system configured library path.'''
146
147        lib_var = {'Linux' : 'LD_LIBRARY_PATH',
148                    'FreeBSD' : 'LD_LIBRARY_PATH',
149                    'Darwin' : 'DYLD_LIBRARY_PATH',
150                    'Windows' : 'PATH'}
151        if not platform.system() in lib_var:
152            sys.stderr('Error: Unsupported platform. Send email to '
153                       'bake_support@inria.fr (%s)' % platform.system())
154            sys.exit(1)
155        return lib_var[platform.system()]
156
157    def _lib_path(self):
158        ''' Returns the value of the library path for the in-use module.'''
159
160        return os.path.join(self._installdir, 'lib')
161
162    def _bin_var(self):
163        return 'PATH'
164    def _bin_path(self):
165        ''' Returns the value of the binary path for the in-use module.'''
166
167        return os.path.join(self._installdir, 'bin')
168
169    def _py_var(self):
170        return 'PYTHONPATH'
171    def _py_path(self):
172        ''' Returns the value of the python path for the in-use module.'''
173
174        return os.path.join(self._installdir, 'lib',
175                            'python'+platform.python_version_tuple()[0]+
176                            '.'+platform.python_version_tuple()[1],
177                            'site-packages')
178
179    def _append_path(self, d, name, value, sep):
180        ''' Append the variable to the system in use configuration. '''
181
182        if not name in d:
183            d[name] = value
184        else:
185            d[name] = d[name] + sep + value
186
187    def start_source(self, name, dir):
188        ''' Sets the environment to be used by the given source module.'''
189
190        assert self._module_supports_objdir is None
191        self._module_name = name
192        self._module_dir = dir
193        self._logger.set_current_module(name)
194
195        # ensure source directory exists
196        if not os.path.isdir(self._sourcedir):
197            os.makedirs(self._sourcedir)
198
199    def end_source(self):
200        ''' Cleans the environment regarding the informations of the last used
201        source module.
202        '''
203
204        self._module_name = None
205        self._module_dir = None
206        self._logger.clear_current_module()
207
208    def start_build(self, name, dir, supports_objdir):
209        ''' Sets the environment to be used by the given build module.'''
210
211#        assert self._module_supports_objdir is None
212        self._module_name = name
213        self._module_dir = dir
214        self._module_supports_objdir = supports_objdir
215        self._logger.set_current_module(name)
216
217        if not os.path.isdir(self.installdir):
218            os.makedirs(self.installdir)
219        if not os.path.isdir(self.objdir):
220            os.makedirs(self.objdir)
221
222    def end_build(self):
223        ''' Cleans the environment regarding the informations of the last used
224        build module.
225        '''
226
227        self._module_name = None
228        self._module_dir = None
229        self._module_supports_objdir = None
230        self._logger.clear_current_module()
231
232    def exist_file(self, file):
233        ''' Finds if the file exists in the path.'''
234
235        return os.path.exists(file)
236
237    def path_list(self):
238        ''' Return path that will be searched for executables '''
239        pythonpath=[]
240
241        if os.environ.get('PYTHONPATH'):
242            pythonpath=os.environ.get('PYTHONPATH').split(os.pathsep)
243        return os.environ.get('PATH').split(os.pathsep) + [self._bin_path()] + pythonpath
244
245    def _program_location(self, program):
246        ''' Finds where the executable is located in the user's path.'''
247
248        # function to verify if the program exists on the given path
249        # and if it is executable
250        def is_exe(path):
251            return os.path.exists(path) and os.access(path, os.X_OK)
252        path, name = os.path.split(program)
253        # if the path for the executable was passed as part of its name
254        if path:
255            if is_exe(program):
256                return program
257        else:
258            # for all the directories in the path search for the executable
259            for path in self.path_list():
260                exe_file = os.path.join(path, program)
261                if is_exe(exe_file):
262                    return exe_file
263
264            toFindIn=None
265            # search for libs with that name on the library path
266            index=program.find(".so") + program.find(".a")
267            if index>0 :
268                toFindIn=['/usr/lib','/usr/lib64','/usr/lib32','/usr/local/lib',
269                     '/lib','/opt/local/lib','/opt/local/Library', '/usr/local/opt']
270                for libpath in self._libpaths:
271                    toFindIn.append(libpath)
272                stdLibs = []
273                try:
274                    libPath = os.environ[self._lib_var()]
275                    if libPath:
276                        stdLibs=libPath.split(os.pathsep)
277                except:
278                    pass
279
280                tofindIn=toFindIn+stdLibs+[self._lib_path()]
281
282            elif program.endswith(".h"):
283                toFindIn=['/usr/include', '/usr/local/include', '/usr/lib','/opt/local/include', '/usr/local/opt']
284
285            if toFindIn :
286                for eachdir in toFindIn:
287                    if sys.platform == "darwin":
288                        # enable symlink walking for MacOS only (bug 2975)
289                        for dirname, dirnames, filenames in os.walk(eachdir, True, None, True):
290                            for filename in filenames:
291                                if filename==name:
292                                    return os.path.join(dirname, filename)
293                    else:
294                        for dirname, dirnames, filenames in os.walk(eachdir):
295                            for filename in filenames:
296                                if filename==name:
297                                    return os.path.join(dirname, filename)
298
299        return None
300
301    def _check_version(self, found, required, match_type):
302        ''' Checks the version of the required executable.'''
303
304        smallerSize=min(len(found),len(required))
305        if match_type == self.HIGHER:
306            for i in range(0,smallerSize):
307                if not found[i]:
308                    return False
309                if int(found[i]) < int(required[i]):
310                    return False
311                elif int(found[i]) > int(required[i]):
312                    return True
313            return True
314        elif match_type == self.LOWER:
315            for i in range(0,smallerSize):
316                if not found[i]:
317                    return True
318                if int(found[i]) > int(required[i]):
319                    return False
320                elif int(found[i]) < int(required[i]):
321                    return True
322            if len(found) >= len(required):
323                return False
324            return True
325        elif match_type == self.EQUAL:
326            if len(found) != len(required):
327                return False
328            for i in range(0,smallerSize):
329                if int(found[i]) != int(required[i]):
330                    return False
331            return True
332        else:
333            assert False
334
335    def add_libpaths(self, libpaths):
336        ''' Adds the list of paths to the in-use library path environment
337        variable.
338        '''
339
340        for element in libpaths :
341            self._libpaths.add(self.replace_variables(element))
342
343    def add_binpaths(self, libpaths):
344        ''' Adds the list of paths to the in-use binary path environment
345        variable.
346        '''
347
348        for element in libpaths :
349            self._binpaths.add(self.replace_variables(element))
350
351    def add_pkgpaths(self, libpaths):
352        ''' Adds the list of paths to the in-use package path environment
353        variable.
354        '''
355
356        for element in libpaths :
357            self._pkgpaths.add(self.replace_variables(element))
358
359    def add_variables(self, libpaths):
360        ''' Adds/replace the list of variables to the in-use set of environment
361        variables.
362        '''
363
364        for element in libpaths :
365            self._variables.add(self.replace_variables(element))
366
367    def create_environment_file(self, fileName):
368        ''' Creates the set environment file to help users to call the Bake
369        built modules.
370        '''
371
372        script = "#!/bin/bash \n#### \n# Environment setting script. Automatically generated by Bake\n####\n\n"
373        script = script + "if [ \"${BASH_SOURCE:-}\" == \"${0}\" ]; then \n" + \
374                 "    echo \"> Call with . bakeSetEnv.sh or source bakeSetEnv.sh\" \n" + \
375                 "    exit 1 \n" + \
376                 "fi \n\n"
377
378        self._binpaths.add(self._bin_path())
379        if os.path.isdir(self._lib_path()):
380            self._libpaths.add(self._lib_path())
381        if os.path.isdir(self._lib_path()+'64'):
382            self._libpaths.add(self._lib_path()+'64')
383
384        if len(self._libpaths) > 0:
385            script = script + self.add_onPath("LD_LIBRARY_PATH", self._libpaths) + "\n"
386
387        if len(self._binpaths) > 0:
388            script = script + self.add_onPath("PATH", self._binpaths) + "\n"
389
390        if len(self._pkgpaths) > 0:
391            script = script + self.add_onPath("PKG_CONFIG_PATH", self._pkgpaths) + "\n"
392
393        from distutils.sysconfig import get_python_lib
394        localLibPath=''
395        libDir=get_python_lib()
396        if libDir:
397            begin=libDir.lower().index('python')
398            localLibPath=os.path.join(self._lib_path(),libDir[begin:])
399
400
401        script = script + self.add_onPath("PYTHONPATH", [sys.path[0],self._lib_path(),localLibPath]) + "\n"
402
403        for element in self._variables:
404            script = script + " export " + element  + "\n"
405
406        fout = open(fileName, "w")
407        fout.write(script)
408        fout.close()
409        os.chmod(fileName, 0o755)
410
411        return script
412
413    def add_onPath (self, variableName, vectorPath):
414        ''' Format the variable to be added on the system.
415        '''
416
417        returnString = " export " + variableName + "=\"${" + variableName + ":+${" + variableName + "}:}"
418        for element in vectorPath:
419            returnString = returnString + element + ":"
420
421        # Strip extra ':'
422        returnString = returnString[:-1]
423
424        returnString = returnString + "\""
425        return returnString
426
427    def replace_variables(self, string):
428        ''' Replace the variables on the string, if they exist, by their
429        system real values.
430        '''
431
432        import re
433        tmp = string
434        tmp = re.sub('\$INSTALLDIR', self.installdir, tmp)
435        tmp = re.sub('\$OBJDIR', self.objdir, tmp)
436        tmp = re.sub('\$SRCDIR', self.srcdir, tmp)
437        return tmp
438
439    def check_program(self, program, version_arg = None,
440                      version_regexp = None, version_required = None,
441                      match_type=HIGHER):
442        '''Checks if the program, with the desired version, exists in the
443        system.
444        '''
445
446        if self._program_location(program) is None:
447            return False
448        if version_arg is None and version_regexp is None and version_required is None:
449            return True
450        else:
451            # This assert as it was avoided the checking of the version of the
452            # executable assert not (version_arg is None or version_regexp is
453            # None or version_required is None)
454            assert not (version_arg is None and version_regexp is None and  version_required is None)
455            popen = subprocess.Popen([self._program_location(program),
456                                        version_arg],
457                                     stdout = subprocess.PIPE,
458                                     stderr = subprocess.STDOUT)
459            (out, err) = popen.communicate('')
460            import re
461            reg = re.compile(version_regexp)
462            for line in out.splitlines():
463                m = reg.search(line)
464                if not m is None:
465                    found = m.groups()
466                    return self._check_version(found, version_required, match_type)
467
468
469    def append_to_path(self, env_vars):
470        """Sets the library and binary paths."""
471
472        for libpath in self._libpaths:
473            self._append_path(env_vars, self._lib_var(), libpath, os.pathsep)
474            if self.debug:
475                print("  -> " + self._lib_var() + " " + libpath + " ")
476
477        self._append_path(env_vars, self._lib_var(), self._lib_path(), os.pathsep)
478        for libpath in self._binpaths:
479            self._append_path(env_vars, self._bin_var(), libpath, os.pathsep)
480            if self.debug:
481                print("  -> " + self._bin_var() + " " + libpath + " ")
482
483        self._append_path(env_vars, self._bin_var(), self._bin_path(), os.pathsep)
484        for libpath in self._pkgpaths:
485            self._append_path(env_vars, self._pkgconfig_var(), libpath, os.pathsep)
486            if self.debug:
487                print("  -> " + self._pkgconfig_var() + " " + libpath + " ")
488
489        self._append_path(env_vars, self._pkgconfig_var(), self._pkgconfig_path(), os.pathsep)
490        self._append_path(env_vars, self._py_var(), self._py_path(), os.pathsep)
491        self._append_path(env_vars, self._py_var(), os.path.join(self._installdir, 'lib'), os.pathsep)
492
493        return env_vars
494
495    def run(self, args, directory = None, env = dict(), interactive = False):
496        '''Executes a system program adding the libraries and over the correct
497        directories.
498        '''
499
500        if not interactive:
501            env_string = ''
502            if len(env) != 0:
503                env_string = ' '.join([a + '=' + b for a,b in env.items()])
504            try:
505                args_string = ' '.join(args)
506            except TypeError as e:
507                raise TaskError('Wrong argument type: %s, expected string,'
508                                ' error: %s' % (str(args), e))
509
510            self._logger.commands.write(env_string + ' ' + args_string +
511                                        ' dir=' + str(directory) + '\n')
512            stdin = None
513            stdout = self._logger.stdout
514            stderr = self._logger.stderr
515        else:
516            stdin = sys.stdin
517            stdout = sys.stdout
518            stderr = sys.stderr
519
520        tmp = dict(list(os.environ.items()) + list(env.items()))
521
522        # sets the library and binary paths
523        tmp = self.append_to_path(tmp)
524
525        # Calls the third party executable with the whole context
526        try:
527            popen = subprocess.Popen(args,
528                                     stdin = stdin,
529                                     stdout = stdout,
530                                     stderr = stderr,
531                                     cwd = directory,
532                                     env = tmp)
533        except Exception as e:
534            raise TaskError('could not execute: %s %s. \nUnexpected error: %s'
535                                % (str(directory), str(args), str(e)))
536
537        # Waits for the full execution of the third party software
538        retcode = popen.wait()
539        if retcode != 0:
540            raise TaskError('Subprocess failed with error %d: %s' % (retcode, str(args)))
541
542
543