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()
33 from shutil
import copy2
, copystat
38 from SCons
.Subst
import SUBST_RAW
39 from SCons
.Tool
.linkCommon
import (
45 # We keep track of *all* installed files.
47 _UNIQUE_INSTALLED_FILES
= None
49 class CopytreeError(OSError):
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
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.
97 names
= os
.listdir(src
)
98 if ignore
is not None:
99 ignored_names
= ignore(src
, names
)
101 ignored_names
= set()
103 os
.makedirs(dst
, exist_ok
=dirs_exist_ok
)
106 if name
in ignored_names
:
108 srcname
= os
.path
.join(src
, name
)
109 dstname
= os
.path
.join(dst
, name
)
111 if os
.path
.islink(srcname
):
112 linkto
= os
.readlink(srcname
)
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
)
120 # ignore dangling symlink if the flag is on
121 if not os
.path
.exists(linkto
) and ignore_dangling_symlinks
:
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
)
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
)
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
)))
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
)))
152 raise CopytreeError(errors
) # SCons change
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.
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
)))
172 parent
= os
.path
.split(dest
)[0]
173 if not os
.path
.exists(parent
):
175 scons_copytree(source
, dest
, dirs_exist_ok
=True)
179 os
.chmod(dest
, stat
.S_IMODE(st
[stat
.ST_MODE
]) | stat
.S_IWRITE
)
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.
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
) )
200 # remove the link if it is already there
207 os
.chmod(dest
, stat
.S_IMODE(st
[stat
.ST_MODE
]) | stat
.S_IWRITE
)
208 installShlibLinks(dest
, source
, env
)
212 def listShlibLinksToInstall(dest
, source
, env
):
214 source
= env
.arg2nodes(source
)
215 dest
= env
.fs
.File(dest
)
216 install_dir
= dest
.get_dir()
218 symlinks
= getattr(getattr(src
, 'attributes', None), 'shliblinks', None)
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
))
228 def installShlibLinks(dest
, source
, env
) -> None:
229 """If we are installing a versioned shared library create the required links."""
231 symlinks
= listShlibLinksToInstall(dest
, source
, env
)
233 print(f
'installShlibLinks: symlinks={StringizeLibSymlinks(symlinks)!r}')
235 CreateLibSymlinks(env
, symlinks
)
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.
244 POSIX-style error code - 0 for success, non-zero for fail
248 install
= env
['INSTALL']
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
):
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.
269 POSIX-style error code - 0 for success, non-zero for fail
273 install
= env
['INSTALLVERSIONEDLIB']
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
)
289 if install(tpath
, s
.get_path(), env
):
294 def stringFunc(target
, source
, env
):
295 installstr
= env
.get('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
):
304 return 'Install %s: "%s" as "%s"' % (type, source
, target
)
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
327 _INSTALLED_FILES
.extend(target
)
329 print(f
"add_versioned_targets_to_INSTALLED_FILES: target={list(map(str, target))!r}")
330 symlinks
= listShlibLinksToInstall(target
[0], source
, env
)
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
340 def __init__(self
, env
, dir) -> None:
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
)
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
):
364 raise SCons
.Errors
.UserError("Both target and dir defined for Install(), only one may be defined.")
369 install_sandbox
= SCons
.Script
.GetOption('install_sandbox')
371 target_factory
= DESTDIR_factory(env
, install_sandbox
)
373 target_factory
= env
.fs
376 dnodes
= env
.arg2nodes(dir, target_factory
.Dir
)
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
)
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
))
391 def InstallAsBuilderWrapper(env
, target
=None, source
=None, **kw
):
393 for src
, tgt
in map(lambda x
, y
: (x
, y
), source
, target
):
394 result
.extend(BaseInstallBuilder(env
, tgt
, src
, **kw
))
397 BaseVersionedInstallBuilder
= None
400 def InstallVersionedBuilderWrapper(env
, target
=None, source
=None, dir=None, **kw
):
403 raise SCons
.Errors
.UserError("Both target and dir defined for Install(), only one may be defined.")
408 install_sandbox
= SCons
.Script
.GetOption('install_sandbox')
410 target_factory
= DESTDIR_factory(env
, install_sandbox
)
412 target_factory
= env
.fs
415 dnodes
= env
.arg2nodes(dir, target_factory
.Dir
)
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
)
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
))
432 def generate(env
) -> None:
434 from SCons
.Script
import AddOption
, GetOption
438 AddOption('--install-sandbox',
439 dest
='install_sandbox',
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')
448 target_factory
= DESTDIR_factory(env
, install_sandbox
)
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
,
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')
465 target_factory
= DESTDIR_factory(env
, install_sandbox
)
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
,
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.
491 # env['INSTALLSTR'] = 'Install ${SOURCE.type}: "$SOURCES" as "$TARGETS"'
496 env
['INSTALL'] = copyFunc
499 env
['INSTALLVERSIONEDLIB']
501 env
['INSTALLVERSIONEDLIB'] = copyFuncVersionedLib
503 def exists(env
) -> bool:
508 # indent-tabs-mode:nil
510 # vim: set expandtab tabstop=4 shiftwidth=4: