renamed SCons.Tool.ninja -> SCons.Tool.ninja_tool and added alias in tool loading...
[scons.git] / SCons / Tool / install.py
blobd553e31afed936893fb31fdf62a7f736d235f8da
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """ Tool-specific initialization for the install tool.
26 There normally shouldn't be any need to import this module directly.
27 It will usually be imported through the generic SCons.Tool.Tool()
28 selection method.
29 """
31 import os
32 import stat
33 from shutil import copy2, copystat
35 import SCons.Action
36 import SCons.Tool
37 import SCons.Util
38 from SCons.Subst import SUBST_RAW
39 from SCons.Tool.linkCommon import (
40 StringizeLibSymlinks,
41 CreateLibSymlinks,
42 EmitLibSymlinks,
45 # We keep track of *all* installed files.
46 _INSTALLED_FILES = []
47 _UNIQUE_INSTALLED_FILES = None
49 class CopytreeError(OSError):
50 pass
53 def scons_copytree(src, dst, symlinks: bool=False, ignore=None, copy_function=copy2,
54 ignore_dangling_symlinks: bool=False, dirs_exist_ok: bool=False):
55 """Recursively copy a directory tree, SCons version.
57 This is a modified copy of the Python 3.7 shutil.copytree function.
58 SCons update: dirs_exist_ok dictates whether to raise an
59 exception in case dst or any missing parent directory already
60 exists. Implementation depends on os.makedirs having a similar
61 flag, which it has since Python 3.2. This version also raises an
62 SCons-defined exception rather than the one defined locally to shtuil.
63 This version uses a change from Python 3.8.
64 TODO: we can remove this forked copy once the minimum Py version is 3.8.
66 If exception(s) occur, an Error is raised with a list of reasons.
68 If the optional symlinks flag is true, symbolic links in the
69 source tree result in symbolic links in the destination tree; if
70 it is false, the contents of the files pointed to by symbolic
71 links are copied. If the file pointed by the symlink doesn't
72 exist, an exception will be added in the list of errors raised in
73 an Error exception at the end of the copy process.
75 You can set the optional ignore_dangling_symlinks flag to true if you
76 want to silence this exception. Notice that this has no effect on
77 platforms that don't support os.symlink.
79 The optional ignore argument is a callable. If given, it
80 is called with the `src` parameter, which is the directory
81 being visited by copytree(), and `names` which is the list of
82 `src` contents, as returned by os.listdir():
84 callable(src, names) -> ignored_names
86 Since copytree() is called recursively, the callable will be
87 called once for each directory that is copied. It returns a
88 list of names relative to the `src` directory that should
89 not be copied.
91 The optional copy_function argument is a callable that will be used
92 to copy each file. It will be called with the source path and the
93 destination path as arguments. By default, copy2() is used, but any
94 function that supports the same signature (like copy()) can be used.
96 """
97 names = os.listdir(src)
98 if ignore is not None:
99 ignored_names = ignore(src, names)
100 else:
101 ignored_names = set()
103 os.makedirs(dst, exist_ok=dirs_exist_ok)
104 errors = []
105 for name in names:
106 if name in ignored_names:
107 continue
108 srcname = os.path.join(src, name)
109 dstname = os.path.join(dst, name)
110 try:
111 if os.path.islink(srcname):
112 linkto = os.readlink(srcname)
113 if symlinks:
114 # We can't just leave it to `copy_function` because legacy
115 # code with a custom `copy_function` may rely on copytree
116 # doing the right thing.
117 os.symlink(linkto, dstname)
118 copystat(srcname, dstname, follow_symlinks=not symlinks)
119 else:
120 # ignore dangling symlink if the flag is on
121 if not os.path.exists(linkto) and ignore_dangling_symlinks:
122 continue
123 # otherwise let the copy occurs. copy2 will raise an error
124 if os.path.isdir(srcname):
125 scons_copytree(srcname, dstname, symlinks=symlinks,
126 ignore=ignore, copy_function=copy_function,
127 ignore_dangling_symlinks=ignore_dangling_symlinks,
128 dirs_exist_ok=dirs_exist_ok)
129 else:
130 copy_function(srcname, dstname)
131 elif os.path.isdir(srcname):
132 scons_copytree(srcname, dstname, symlinks=symlinks,
133 ignore=ignore, copy_function=copy_function,
134 ignore_dangling_symlinks=ignore_dangling_symlinks,
135 dirs_exist_ok=dirs_exist_ok)
136 else:
137 # Will raise a SpecialFileError for unsupported file types
138 copy_function(srcname, dstname)
139 # catch the Error from the recursive copytree so that we can
140 # continue with other files
141 except CopytreeError as err: # SCons change
142 errors.extend(err.args[0])
143 except OSError as why:
144 errors.append((srcname, dstname, str(why)))
145 try:
146 copystat(src, dst)
147 except OSError as why:
148 # Copying file access times may fail on Windows
149 if getattr(why, 'winerror', None) is None:
150 errors.append((src, dst, str(why)))
151 if errors:
152 raise CopytreeError(errors) # SCons change
153 return dst
156 # Functions doing the actual work of the Install Builder.
158 def copyFunc(dest, source, env) -> int:
159 """Install a source file or directory into a destination by copying.
161 Mode/permissions bits will be copied as well, except that the target
162 will be made writable.
164 Returns:
165 POSIX-style error code - 0 for success, non-zero for fail
167 if os.path.isdir(source):
168 if os.path.exists(dest):
169 if not os.path.isdir(dest):
170 raise SCons.Errors.UserError("cannot overwrite non-directory `%s' with a directory `%s'" % (str(dest), str(source)))
171 else:
172 parent = os.path.split(dest)[0]
173 if not os.path.exists(parent):
174 os.makedirs(parent)
175 scons_copytree(source, dest, dirs_exist_ok=True)
176 else:
177 copy2(source, dest)
178 st = os.stat(source)
179 os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
181 return 0
184 # Functions doing the actual work of the InstallVersionedLib Builder.
186 def copyFuncVersionedLib(dest, source, env) -> int:
187 """Install a versioned library into a destination by copying.
189 Any required symbolic links for other library names are created.
191 Mode/permissions bits will be copied as well, except that the target
192 will be made writable.
194 Returns:
195 POSIX-style error code - 0 for success, non-zero for fail
197 if os.path.isdir(source):
198 raise SCons.Errors.UserError("cannot install directory `%s' as a version library" % str(source) )
199 else:
200 # remove the link if it is already there
201 try:
202 os.remove(dest)
203 except:
204 pass
205 copy2(source, dest)
206 st = os.stat(source)
207 os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
208 installShlibLinks(dest, source, env)
210 return 0
212 def listShlibLinksToInstall(dest, source, env):
213 install_links = []
214 source = env.arg2nodes(source)
215 dest = env.fs.File(dest)
216 install_dir = dest.get_dir()
217 for src in source:
218 symlinks = getattr(getattr(src, 'attributes', None), 'shliblinks', None)
219 if symlinks:
220 for link, linktgt in symlinks:
221 link_base = os.path.basename(link.get_path())
222 linktgt_base = os.path.basename(linktgt.get_path())
223 install_link = env.fs.File(link_base, install_dir)
224 install_linktgt = env.fs.File(linktgt_base, install_dir)
225 install_links.append((install_link, install_linktgt))
226 return install_links
228 def installShlibLinks(dest, source, env) -> None:
229 """If we are installing a versioned shared library create the required links."""
230 Verbose = False
231 symlinks = listShlibLinksToInstall(dest, source, env)
232 if Verbose:
233 print(f'installShlibLinks: symlinks={StringizeLibSymlinks(symlinks)!r}')
234 if symlinks:
235 CreateLibSymlinks(env, symlinks)
236 return
238 def installFunc(target, source, env) -> int:
239 """Install a source file into a target.
241 Uses the function specified in the INSTALL construction variable.
243 Returns:
244 POSIX-style error code - 0 for success, non-zero for fail
247 try:
248 install = env['INSTALL']
249 except KeyError:
250 raise SCons.Errors.UserError('Missing INSTALL construction variable.')
252 assert len(target) == len(source), (
253 "Installing source %s into target %s: "
254 "target and source lists must have same length."
255 % (list(map(str, source)), list(map(str, target)))
257 for t, s in zip(target, source):
258 if install(t.get_path(), s.get_path(), env):
259 return 1
261 return 0
263 def installFuncVersionedLib(target, source, env) -> int:
264 """Install a versioned library into a target.
266 Uses the function specified in the INSTALL construction variable.
268 Returns:
269 POSIX-style error code - 0 for success, non-zero for fail
272 try:
273 install = env['INSTALLVERSIONEDLIB']
274 except KeyError:
275 raise SCons.Errors.UserError(
276 'Missing INSTALLVERSIONEDLIB construction variable.'
279 assert len(target) == len(source), (
280 "Installing source %s into target %s: "
281 "target and source lists must have same length."
282 % (list(map(str, source)), list(map(str, target)))
284 for t, s in zip(target, source):
285 if hasattr(t.attributes, 'shlibname'):
286 tpath = os.path.join(t.get_dir(), t.attributes.shlibname)
287 else:
288 tpath = t.get_path()
289 if install(tpath, s.get_path(), env):
290 return 1
292 return 0
294 def stringFunc(target, source, env):
295 installstr = env.get('INSTALLSTR')
296 if installstr:
297 return env.subst_target_source(installstr, SUBST_RAW, target, source)
298 target = str(target[0])
299 source = str(source[0])
300 if os.path.isdir(source):
301 type = 'directory'
302 else:
303 type = 'file'
304 return 'Install %s: "%s" as "%s"' % (type, source, target)
307 # Emitter functions
309 def add_targets_to_INSTALLED_FILES(target, source, env):
310 """ An emitter that adds all target files to the list stored in the
311 _INSTALLED_FILES global variable. This way all installed files of one
312 scons call will be collected.
314 global _INSTALLED_FILES, _UNIQUE_INSTALLED_FILES
315 _INSTALLED_FILES.extend(target)
317 _UNIQUE_INSTALLED_FILES = None
318 return (target, source)
320 def add_versioned_targets_to_INSTALLED_FILES(target, source, env):
321 """ An emitter that adds all target files to the list stored in the
322 _INSTALLED_FILES global variable. This way all installed files of one
323 scons call will be collected.
325 global _INSTALLED_FILES, _UNIQUE_INSTALLED_FILES
326 Verbose = False
327 _INSTALLED_FILES.extend(target)
328 if Verbose:
329 print(f"add_versioned_targets_to_INSTALLED_FILES: target={list(map(str, target))!r}")
330 symlinks = listShlibLinksToInstall(target[0], source, env)
331 if symlinks:
332 EmitLibSymlinks(env, symlinks, target[0])
333 _UNIQUE_INSTALLED_FILES = None
334 return (target, source)
336 class DESTDIR_factory:
337 """ A node factory, where all files will be relative to the dir supplied
338 in the constructor.
340 def __init__(self, env, dir) -> None:
341 self.env = env
342 self.dir = env.arg2nodes( dir, env.fs.Dir )[0]
344 def Entry(self, name):
345 name = SCons.Util.make_path_relative(name)
346 return self.dir.Entry(name)
348 def Dir(self, name):
349 name = SCons.Util.make_path_relative(name)
350 return self.dir.Dir(name)
353 # The Builder Definition
355 install_action = SCons.Action.Action(installFunc, stringFunc)
356 installas_action = SCons.Action.Action(installFunc, stringFunc)
357 installVerLib_action = SCons.Action.Action(installFuncVersionedLib, stringFunc)
359 BaseInstallBuilder = None
361 def InstallBuilderWrapper(env, target=None, source=None, dir=None, **kw):
362 if target and dir:
363 import SCons.Errors
364 raise SCons.Errors.UserError("Both target and dir defined for Install(), only one may be defined.")
365 if not dir:
366 dir=target
368 import SCons.Script
369 install_sandbox = SCons.Script.GetOption('install_sandbox')
370 if install_sandbox:
371 target_factory = DESTDIR_factory(env, install_sandbox)
372 else:
373 target_factory = env.fs
375 try:
376 dnodes = env.arg2nodes(dir, target_factory.Dir)
377 except TypeError:
378 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))
379 sources = env.arg2nodes(source, env.fs.Entry)
380 tgt = []
381 for dnode in dnodes:
382 for src in sources:
383 # Prepend './' so the lookup doesn't interpret an initial
384 # '#' on the file name portion as meaning the Node should
385 # be relative to the top-level SConstruct directory.
386 target = env.fs.Entry('.'+os.sep+src.name, dnode)
387 tgt.extend(BaseInstallBuilder(env, target, src, **kw))
388 return tgt
391 def InstallAsBuilderWrapper(env, target=None, source=None, **kw):
392 result = []
393 for src, tgt in map(lambda x, y: (x, y), source, target):
394 result.extend(BaseInstallBuilder(env, tgt, src, **kw))
395 return result
397 BaseVersionedInstallBuilder = None
400 def InstallVersionedBuilderWrapper(env, target=None, source=None, dir=None, **kw):
401 if target and dir:
402 import SCons.Errors
403 raise SCons.Errors.UserError("Both target and dir defined for Install(), only one may be defined.")
404 if not dir:
405 dir=target
407 import SCons.Script
408 install_sandbox = SCons.Script.GetOption('install_sandbox')
409 if install_sandbox:
410 target_factory = DESTDIR_factory(env, install_sandbox)
411 else:
412 target_factory = env.fs
414 try:
415 dnodes = env.arg2nodes(dir, target_factory.Dir)
416 except TypeError:
417 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))
418 sources = env.arg2nodes(source, env.fs.Entry)
419 tgt = []
420 for dnode in dnodes:
421 for src in sources:
422 # Prepend './' so the lookup doesn't interpret an initial
423 # '#' on the file name portion as meaning the Node should
424 # be relative to the top-level SConstruct directory.
425 target = env.fs.Entry('.'+os.sep+src.name, dnode)
426 tgt.extend(BaseVersionedInstallBuilder(env, target, src, **kw))
427 return tgt
429 added = None
432 def generate(env) -> None:
434 from SCons.Script import AddOption, GetOption
435 global added
436 if not added:
437 added = 1
438 AddOption('--install-sandbox',
439 dest='install_sandbox',
440 type="string",
441 action="store",
442 help='A directory under which all installed files will be placed.')
444 global BaseInstallBuilder
445 if BaseInstallBuilder 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
452 BaseInstallBuilder = SCons.Builder.Builder(
453 action = install_action,
454 target_factory = target_factory.Entry,
455 source_factory = env.fs.Entry,
456 multi = True,
457 emitter = [ add_targets_to_INSTALLED_FILES, ],
458 source_scanner = SCons.Scanner.ScannerBase({}, name='Install', recursive=False),
459 name = 'InstallBuilder')
461 global BaseVersionedInstallBuilder
462 if BaseVersionedInstallBuilder is None:
463 install_sandbox = GetOption('install_sandbox')
464 if install_sandbox:
465 target_factory = DESTDIR_factory(env, install_sandbox)
466 else:
467 target_factory = env.fs
469 BaseVersionedInstallBuilder = SCons.Builder.Builder(
470 action = installVerLib_action,
471 target_factory = target_factory.Entry,
472 source_factory = env.fs.Entry,
473 multi = True,
474 emitter = [ add_versioned_targets_to_INSTALLED_FILES, ],
475 name = 'InstallVersionedBuilder')
477 env['BUILDERS']['_InternalInstall'] = InstallBuilderWrapper
478 env['BUILDERS']['_InternalInstallAs'] = InstallAsBuilderWrapper
479 env['BUILDERS']['_InternalInstallVersionedLib'] = InstallVersionedBuilderWrapper
481 # We'd like to initialize this doing something like the following,
482 # but there isn't yet support for a ${SOURCE.type} expansion that
483 # will print "file" or "directory" depending on what's being
484 # installed. For now we punt by not initializing it, and letting
485 # the stringFunc() that we put in the action fall back to the
486 # hand-crafted default string if it's not set.
488 #try:
489 # env['INSTALLSTR']
490 #except KeyError:
491 # env['INSTALLSTR'] = 'Install ${SOURCE.type}: "$SOURCES" as "$TARGETS"'
493 try:
494 env['INSTALL']
495 except KeyError:
496 env['INSTALL'] = copyFunc
498 try:
499 env['INSTALLVERSIONEDLIB']
500 except KeyError:
501 env['INSTALLVERSIONEDLIB'] = copyFuncVersionedLib
503 def exists(env) -> bool:
504 return True
506 # Local Variables:
507 # tab-width:4
508 # indent-tabs-mode:nil
509 # End:
510 # vim: set expandtab tabstop=4 shiftwidth=4: