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