1"""SCons.Tool.install
2
3Tool-specific initialization for the install tool.
4
5There normally shouldn't be any need to import this module directly.
6It will usually be imported through the generic SCons.Tool.Tool()
7selection method.
8"""
9
10#
11# Copyright (c) 2001 - 2014 The SCons Foundation
12#
13# Permission is hereby granted, free of charge, to any person obtaining
14# a copy of this software and associated documentation files (the
15# "Software"), to deal in the Software without restriction, including
16# without limitation the rights to use, copy, modify, merge, publish,
17# distribute, sublicense, and/or sell copies of the Software, and to
18# permit persons to whom the Software is furnished to do so, subject to
19# the following conditions:
20#
21# The above copyright notice and this permission notice shall be included
22# in all copies or substantial portions of the Software.
23#
24# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
25# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
26# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31#
32
33__revision__ = "src/engine/SCons/Tool/install.py  2014/08/24 12:12:31 garyo"
34
35import os
36import re
37import shutil
38import stat
39
40import SCons.Action
41from SCons.Util import make_path_relative
42
43#
44# We keep track of *all* installed files.
45_INSTALLED_FILES = []
46_UNIQUE_INSTALLED_FILES = None
47
48class CopytreeError(EnvironmentError):
49    pass
50
51# This is a patched version of shutil.copytree from python 2.5.  It
52# doesn't fail if the dir exists, which regular copytree does
53# (annoyingly).  Note the XXX comment in the docstring.
54def scons_copytree(src, dst, symlinks=False):
55    """Recursively copy a directory tree using copy2().
56
57    The destination directory must not already exist.
58    If exception(s) occur, an CopytreeError is raised with a list of reasons.
59
60    If the optional symlinks flag is true, symbolic links in the
61    source tree result in symbolic links in the destination tree; if
62    it is false, the contents of the files pointed to by symbolic
63    links are copied.
64
65    XXX Consider this example code rather than the ultimate tool.
66
67    """
68    names = os.listdir(src)
69    # garyo@genarts.com fix: check for dir before making dirs.
70    if not os.path.exists(dst):
71        os.makedirs(dst)
72    errors = []
73    for name in names:
74        srcname = os.path.join(src, name)
75        dstname = os.path.join(dst, name)
76        try:
77            if symlinks and os.path.islink(srcname):
78                linkto = os.readlink(srcname)
79                os.symlink(linkto, dstname)
80            elif os.path.isdir(srcname):
81                scons_copytree(srcname, dstname, symlinks)
82            else:
83                shutil.copy2(srcname, dstname)
84            # XXX What about devices, sockets etc.?
85        except (IOError, os.error), why:
86            errors.append((srcname, dstname, str(why)))
87        # catch the CopytreeError from the recursive copytree so that we can
88        # continue with other files
89        except CopytreeError, err:
90            errors.extend(err.args[0])
91    try:
92        shutil.copystat(src, dst)
93    except WindowsError:
94        # can't copy file access times on Windows
95        pass
96    except OSError, why:
97        errors.extend((src, dst, str(why)))
98    if errors:
99        raise CopytreeError, errors
100
101
102#
103# Functions doing the actual work of the Install Builder.
104#
105def copyFunc(dest, source, env):
106    """Install a source file or directory into a destination by copying,
107    (including copying permission/mode bits)."""
108
109    if os.path.isdir(source):
110        if os.path.exists(dest):
111            if not os.path.isdir(dest):
112                raise SCons.Errors.UserError("cannot overwrite non-directory `%s' with a directory `%s'" % (str(dest), str(source)))
113        else:
114            parent = os.path.split(dest)[0]
115            if not os.path.exists(parent):
116                os.makedirs(parent)
117        scons_copytree(source, dest)
118    else:
119        shutil.copy2(source, dest)
120        st = os.stat(source)
121        os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
122
123    return 0
124
125#
126# Functions doing the actual work of the InstallVersionedLib Builder.
127#
128def copyFuncVersionedLib(dest, source, env):
129    """Install a versioned library into a destination by copying,
130    (including copying permission/mode bits) and then creating
131    required symlinks."""
132
133    if os.path.isdir(source):
134        raise SCons.Errors.UserError("cannot install directory `%s' as a version library" % str(source) )
135    else:
136        # remove the link if it is already there
137        try:
138            os.remove(dest)
139        except:
140            pass
141        shutil.copy2(source, dest)
142        st = os.stat(source)
143        os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
144        versionedLibLinks(dest, source, env)
145
146    return 0
147
148def versionedLibVersion(dest, env):
149    """Check if dest is a version shared library name. Return version, libname, & install_dir if it is."""
150    Verbose = False
151    platform = env.subst('$PLATFORM')
152    if not (platform == 'posix'  or platform == 'darwin'):
153        return (None, None, None)
154
155    libname = os.path.basename(dest)
156    install_dir = os.path.dirname(dest)
157    shlib_suffix = env.subst('$SHLIBSUFFIX')
158    # See if the source name is a versioned shared library, get the version number
159    result = False
160
161    version_re = re.compile("[0-9]+\\.[0-9]+\\.[0-9a-zA-Z]+")
162    version_File = None
163    if platform == 'posix':
164        # handle unix names
165        versioned_re = re.compile(re.escape(shlib_suffix + '.') + "[0-9]+\\.[0-9]+\\.[0-9a-zA-Z]+")
166        result = versioned_re.findall(libname)
167        if result:
168            version_File = version_re.findall(versioned_re.findall(libname)[-1])[-1]
169    elif platform == 'darwin':
170        # handle OSX names
171        versioned_re = re.compile("\\.[0-9]+\\.[0-9]+\\.[0-9a-zA-Z]+" + re.escape(shlib_suffix) )
172        result = versioned_re.findall(libname)
173        if result:
174            version_File = version_re.findall(versioned_re.findall(libname)[-1])[-1]
175
176    if Verbose:
177        print "install: version_File ", version_File
178    # result is False if we did not find a versioned shared library name, so return and empty list
179    if not result:
180        return (None, libname, install_dir)
181
182    version = None
183    # get version number from the environment
184    try:
185        version = env.subst('$SHLIBVERSION')
186    except KeyError:
187        version = None
188
189    if version != version_File:
190        #raise SCons.Errors.UserError("SHLIBVERSION '%s' does not match the version # '%s' in the filename" % (version, version_File) )
191        print "SHLIBVERSION '%s' does not match the version # '%s' in the filename, proceeding based on file name" % (version, version_File)
192        version = version_File
193    return (version, libname, install_dir)
194
195def versionedLibLinks(dest, source, env):
196    """If we are installing a versioned shared library create the required links."""
197    Verbose = False
198    linknames = []
199    version, libname, install_dir = versionedLibVersion(dest, env)
200
201    if version != None:
202        # libname includes the version number if one was given
203        linknames = SCons.Tool.VersionShLibLinkNames(version,libname,env)
204        if Verbose:
205            print "versionedLibLinks: linknames ",linknames
206        # Here we just need the file name w/o path as the target of the link
207        lib_ver = libname
208        # make symlink of adjacent names in linknames
209        for count in range(len(linknames)):
210            linkname = linknames[count]
211            fulllinkname = os.path.join(install_dir, linkname)
212            if Verbose:
213                print "full link name ",fulllinkname
214            if count > 0:
215                try:
216                    os.remove(lastlinkname)
217                except:
218                    pass
219                os.symlink(os.path.basename(fulllinkname),lastlinkname)
220                if Verbose:
221                    print "versionedLibLinks: made sym link of %s -> %s" % (lastlinkname,os.path.basename(fulllinkname))
222            lastlinkname = fulllinkname
223        # finish chain of sym links with link to the actual library
224        if len(linknames)>0:
225            try:
226                os.remove(lastlinkname)
227            except:
228                pass
229            os.symlink(lib_ver,lastlinkname)
230            if Verbose:
231                print "versionedLibLinks: made sym link of %s -> %s" % (lib_ver,lastlinkname)
232    return
233
234def installFunc(target, source, env):
235    """Install a source file into a target using the function specified
236    as the INSTALL construction variable."""
237    try:
238        install = env['INSTALL']
239    except KeyError:
240        raise SCons.Errors.UserError('Missing INSTALL construction variable.')
241
242    assert len(target)==len(source), \
243           "Installing source %s into target %s: target and source lists must have same length."%(list(map(str, source)), list(map(str, target)))
244    for t,s in zip(target,source):
245        if install(t.get_path(),s.get_path(),env):
246            return 1
247
248    return 0
249
250def installFuncVersionedLib(target, source, env):
251    """Install a versioned library into a target using the function specified
252    as the INSTALLVERSIONEDLIB construction variable."""
253    try:
254        install = env['INSTALLVERSIONEDLIB']
255    except KeyError:
256        raise SCons.Errors.UserError('Missing INSTALLVERSIONEDLIB construction variable.')
257
258    assert len(target)==len(source), \
259           "Installing source %s into target %s: target and source lists must have same length."%(list(map(str, source)), list(map(str, target)))
260    for t,s in zip(target,source):
261        if install(t.get_path(),s.get_path(),env):
262            return 1
263
264    return 0
265
266def stringFunc(target, source, env):
267    installstr = env.get('INSTALLSTR')
268    if installstr:
269        return env.subst_target_source(installstr, 0, target, source)
270    target = str(target[0])
271    source = str(source[0])
272    if os.path.isdir(source):
273        type = 'directory'
274    else:
275        type = 'file'
276    return 'Install %s: "%s" as "%s"' % (type, source, target)
277
278#
279# Emitter functions
280#
281def add_targets_to_INSTALLED_FILES(target, source, env):
282    """ an emitter that adds all target files to the list stored in the
283    _INSTALLED_FILES global variable. This way all installed files of one
284    scons call will be collected.
285    """
286    global _INSTALLED_FILES, _UNIQUE_INSTALLED_FILES
287    _INSTALLED_FILES.extend(target)
288
289    _UNIQUE_INSTALLED_FILES = None
290    return (target, source)
291
292def add_versioned_targets_to_INSTALLED_FILES(target, source, env):
293    """ an emitter that adds all target files to the list stored in the
294    _INSTALLED_FILES global variable. This way all installed files of one
295    scons call will be collected.
296    """
297    global _INSTALLED_FILES, _UNIQUE_INSTALLED_FILES
298    Verbose = False
299    _INSTALLED_FILES.extend(target)
300    if Verbose:
301        print "ver lib emitter ",repr(target)
302
303    # see if we have a versioned shared library, if so generate side effects
304    version, libname, install_dir = versionedLibVersion(target[0].path, env)
305    if version != None:
306        # generate list of link names
307        linknames = SCons.Tool.VersionShLibLinkNames(version,libname,env)
308        for linkname in linknames:
309            if Verbose:
310                print "make side effect of %s" % os.path.join(install_dir, linkname)
311            fulllinkname = os.path.join(install_dir, linkname)
312            env.SideEffect(fulllinkname,target[0])
313            env.Clean(target[0],fulllinkname)
314            _INSTALLED_FILES.append(fulllinkname)
315            if Verbose:
316                print "installed list ", _INSTALLED_FILES
317
318    _UNIQUE_INSTALLED_FILES = None
319    return (target, source)
320
321class DESTDIR_factory(object):
322    """ a node factory, where all files will be relative to the dir supplied
323    in the constructor.
324    """
325    def __init__(self, env, dir):
326        self.env = env
327        self.dir = env.arg2nodes( dir, env.fs.Dir )[0]
328
329    def Entry(self, name):
330        name = make_path_relative(name)
331        return self.dir.Entry(name)
332
333    def Dir(self, name):
334        name = make_path_relative(name)
335        return self.dir.Dir(name)
336
337#
338# The Builder Definition
339#
340install_action       = SCons.Action.Action(installFunc, stringFunc)
341installas_action     = SCons.Action.Action(installFunc, stringFunc)
342installVerLib_action = SCons.Action.Action(installFuncVersionedLib, stringFunc)
343
344BaseInstallBuilder               = None
345
346def InstallBuilderWrapper(env, target=None, source=None, dir=None, **kw):
347    if target and dir:
348        import SCons.Errors
349        raise SCons.Errors.UserError("Both target and dir defined for Install(), only one may be defined.")
350    if not dir:
351        dir=target
352
353    import SCons.Script
354    install_sandbox = SCons.Script.GetOption('install_sandbox')
355    if install_sandbox:
356        target_factory = DESTDIR_factory(env, install_sandbox)
357    else:
358        target_factory = env.fs
359
360    try:
361        dnodes = env.arg2nodes(dir, target_factory.Dir)
362    except TypeError:
363        raise SCons.Errors.UserError("Target `%s' of Install() is a file, but should be a directory.  Perhaps you have the Install() arguments backwards?" % str(dir))
364    sources = env.arg2nodes(source, env.fs.Entry)
365    tgt = []
366    for dnode in dnodes:
367        for src in sources:
368            # Prepend './' so the lookup doesn't interpret an initial
369            # '#' on the file name portion as meaning the Node should
370            # be relative to the top-level SConstruct directory.
371            target = env.fs.Entry('.'+os.sep+src.name, dnode)
372            #tgt.extend(BaseInstallBuilder(env, target, src, **kw))
373            tgt.extend(BaseInstallBuilder(env, target, src, **kw))
374    return tgt
375
376def InstallAsBuilderWrapper(env, target=None, source=None, **kw):
377    result = []
378    for src, tgt in map(lambda x, y: (x, y), source, target):
379        #result.extend(BaseInstallBuilder(env, tgt, src, **kw))
380        result.extend(BaseInstallBuilder(env, tgt, src, **kw))
381    return result
382
383BaseVersionedInstallBuilder = None
384
385def InstallVersionedBuilderWrapper(env, target=None, source=None, dir=None, **kw):
386    if target and dir:
387        import SCons.Errors
388        raise SCons.Errors.UserError("Both target and dir defined for Install(), only one may be defined.")
389    if not dir:
390        dir=target
391
392    import SCons.Script
393    install_sandbox = SCons.Script.GetOption('install_sandbox')
394    if install_sandbox:
395        target_factory = DESTDIR_factory(env, install_sandbox)
396    else:
397        target_factory = env.fs
398
399    try:
400        dnodes = env.arg2nodes(dir, target_factory.Dir)
401    except TypeError:
402        raise SCons.Errors.UserError("Target `%s' of Install() is a file, but should be a directory.  Perhaps you have the Install() arguments backwards?" % str(dir))
403    sources = env.arg2nodes(source, env.fs.Entry)
404    tgt = []
405    for dnode in dnodes:
406        for src in sources:
407            # Prepend './' so the lookup doesn't interpret an initial
408            # '#' on the file name portion as meaning the Node should
409            # be relative to the top-level SConstruct directory.
410            target = env.fs.Entry('.'+os.sep+src.name, dnode)
411            tgt.extend(BaseVersionedInstallBuilder(env, target, src, **kw))
412    return tgt
413
414added = None
415
416def generate(env):
417
418    from SCons.Script import AddOption, GetOption
419    global added
420    if not added:
421        added = 1
422        AddOption('--install-sandbox',
423                  dest='install_sandbox',
424                  type="string",
425                  action="store",
426                  help='A directory under which all installed files will be placed.')
427
428    global BaseInstallBuilder
429    if BaseInstallBuilder is None:
430        install_sandbox = GetOption('install_sandbox')
431        if install_sandbox:
432            target_factory = DESTDIR_factory(env, install_sandbox)
433        else:
434            target_factory = env.fs
435
436        BaseInstallBuilder = SCons.Builder.Builder(
437                              action         = install_action,
438                              target_factory = target_factory.Entry,
439                              source_factory = env.fs.Entry,
440                              multi          = 1,
441                              emitter        = [ add_targets_to_INSTALLED_FILES, ],
442                              name           = 'InstallBuilder')
443
444    global BaseVersionedInstallBuilder
445    if BaseVersionedInstallBuilder is None:
446        install_sandbox = GetOption('install_sandbox')
447        if install_sandbox:
448            target_factory = DESTDIR_factory(env, install_sandbox)
449        else:
450            target_factory = env.fs
451
452        BaseVersionedInstallBuilder = SCons.Builder.Builder(
453                                       action         = installVerLib_action,
454                                       target_factory = target_factory.Entry,
455                                       source_factory = env.fs.Entry,
456                                       multi          = 1,
457                                       emitter        = [ add_versioned_targets_to_INSTALLED_FILES, ],
458                                       name           = 'InstallVersionedBuilder')
459
460    env['BUILDERS']['_InternalInstall'] = InstallBuilderWrapper
461    env['BUILDERS']['_InternalInstallAs'] = InstallAsBuilderWrapper
462    env['BUILDERS']['_InternalInstallVersionedLib'] = InstallVersionedBuilderWrapper
463
464    # We'd like to initialize this doing something like the following,
465    # but there isn't yet support for a ${SOURCE.type} expansion that
466    # will print "file" or "directory" depending on what's being
467    # installed.  For now we punt by not initializing it, and letting
468    # the stringFunc() that we put in the action fall back to the
469    # hand-crafted default string if it's not set.
470    #
471    #try:
472    #    env['INSTALLSTR']
473    #except KeyError:
474    #    env['INSTALLSTR'] = 'Install ${SOURCE.type}: "$SOURCES" as "$TARGETS"'
475
476    try:
477        env['INSTALL']
478    except KeyError:
479        env['INSTALL']    = copyFunc
480
481    try:
482        env['INSTALLVERSIONEDLIB']
483    except KeyError:
484        env['INSTALLVERSIONEDLIB']    = copyFuncVersionedLib
485
486def exists(env):
487    return 1
488
489# Local Variables:
490# tab-width:4
491# indent-tabs-mode:nil
492# End:
493# vim: set expandtab tabstop=4 shiftwidth=4:
494