InstallFiles is a pseudo-builder that scans a source directory for files matching a certain pattern and installs the results in a destination direction. Support for glob patterns, exclude patterns, and recursion are provided. Glob patterns only apply to files, but the exclude patterns also apply to directories. If the source is a file, it is copied to the destination. If the source is a directory, it is scanned, and items in that directory are copied to the destination. This uses the InstallAs builder, so using --install-sandbox=/path/to/sandbox will affect it as well.
Sample usage:
1 TOOL_INSTALL(env)
2
3 # Prevent installation of certain undesired items
4 env.InstallExclude('.*', '*~', '*.tmp', '*.bak', '*.pyc', '*.pyo')
5
6 # Install a file to a destination. When the source is a file
7 # glob/exclude/recursion is not important
8 env.InstallFiles('$PREFIX/bin', program)
9
10 # Install contents of a directory. When the source is a directory
11 # glob/exclude/recursion is important and will affect items in it
12 env.InstallFiles('$PREFIX/share/myapp/pixmaps', '#data/pixmaps')
13
14 # You can also specify a glob to match or additional excludes in
15 # addition to those specified with InstallExclude
16 env.InstallFiles('$PREFIX/share/myapp/help', '#data/help', glob=['*.html', '*.png'], exclude=['*.svg'])
In addition you can tell it to create named packages of files and then later install those packages into the correct location.
1 # File: data/SConscript:
2
3 # Make files under 'pixmaps' and 'pixmaps-extra' install to the 'pixmaps' directory of the 'data' package
4 env.InstallPackageAccum('data', 'pixmaps', 'pixmaps', scan=1)
5 env.InstallPackageAccum('data', 'pixmaps', 'pixmaps-extra', scan=1)
6
7 # File: installers/linux/SConscript:
8
9 # Install the 'data' package to the data directory
10 env.InstallPackage('/usr/share/myapplication', 'data')
The new builder code that also scans the build nodes as well
1 # File: install.py
2 # Author: Brian A. Vanderburg II
3 # Purpose: An install builder to install subdirectories and files
4 # Copyright: This file is placed in the public domain.
5 ##############################################################################
6
7
8 # Requirements
9 ##############################################################################
10 import os
11 import fnmatch
12
13 import SCons
14 import SCons.Node.FS
15 import SCons.Errors
16 from SCons.Script import *
17
18
19 # Install files code
20 ##############################################################################
21
22 def _is_excluded(name, exclude):
23 """
24 Determine if the name is to be excluded based on the exclusion list
25 """
26 if not exclude:
27 return False
28 return any( (fnmatch.fnmatchcase(name, i) for i in exclude) )
29
30
31 def _is_globbed(name, glob):
32 """
33 Determine if the name is globbed based on the glob list
34 """
35 if not glob:
36 return True
37 return any( (fnmatch.fnmatchcase(name, i) for i in glob) )
38
39
40 def _get_files(env, source, exclude, glob, recursive, reldir=os.curdir):
41 """
42 Find files that match the criteria.
43
44 source - absolute path to source file or directory (not a node)
45 exclude - additional exclusion masks in addition to default
46 glob - glob masks
47 recursive - scan recursively or not
48
49 Returns a list of 2-element tuples where the first element is
50 relative to the source, the second is the absolute file name.
51 """
52
53 # Make sure it exists and is not a link
54 if not os.path.exists(source):
55 return []
56
57 if os.path.islink(source):
58 return []
59
60 # Handle file directly
61 if os.path.isfile(source):
62 return [ (os.path.join(reldir, os.path.basename(source)), source) ]
63
64 # Scan source files on disk
65 if not os.path.isdir(source):
66 return []
67
68 results = []
69 for entry in os.listdir(source):
70 fullpath = os.path.join(source, entry)
71 if os.path.islink(fullpath):
72 continue
73
74 # Excluded (both files and directories)
75 if _is_excluded(entry, exclude):
76 continue
77
78 # File
79 if os.path.isfile(fullpath):
80 if _is_globbed(entry, glob):
81 results.append( (os.path.join(reldir, entry), fullpath) )
82 elif os.path.isdir(fullpath):
83 if recursive:
84 newrel = os.path.join(reldir, entry)
85 results.extend(_get_files(env, fullpath, exclude, glob, recursive, newrel))
86
87 return results
88
89
90 def _get_built_files(env, source, exclude, glob, recursive, reldir=os.curdir):
91 """
92 Find files that match the criteria.
93
94 source - source file or directory node
95 exclude - additional exclusion masks in addition to default
96 glob - glob masks
97 recursive - scan recursively or not
98
99 Returns a list of 2-element tuples where the first element is
100 relative to the source, the second is the absolute file name.
101 """
102
103 # Source
104 source = source.disambiguate()
105
106 # If a file, return it without scanning
107 if isinstance(source, SCons.Node.FS.File):
108 if source.is_derived():
109 source = source.abspath
110 return [ (os.path.join(reldir, os.path.basename(source)), source) ]
111 else:
112 return []
113
114 if not isinstance(source, SCons.Node.FS.Dir):
115 return []
116
117 # Walk the children
118 results = []
119
120 for child in source.children():
121 child = child.disambiguate()
122 name = os.path.basename(child.abspath)
123
124 # Ignore '.' and '..'
125 if name == '.' or name == '..':
126 continue
127
128 # Exclude applies to files and directories
129 if _is_excluded(name, exclude):
130 continue
131
132 if isinstance(child, SCons.Node.FS.File):
133 if child.is_derived() and _is_globbed(name, glob):
134 results.append( (os.path.join(reldir, name), child.abspath) )
135 elif isinstance(child, SCons.Node.FS.Dir):
136 if recursive:
137 newrel = os.path.join(reldir, name)
138 results.extend(_get_built_files(env, child, exclude, glob, recursive, newrel))
139
140 return results
141
142
143 def _get_both(env, source, exclude, glob, recursive, scan):
144 """
145 Get both the built and source files that match the criteria. Built
146 files take priority over a source file of the same path and name.
147 """
148
149 src_nodes = []
150 results = []
151
152 # Get built files
153 if scan == 0 or scan == 2:
154 results.extend(_get_built_files(env, source, exclude, glob, recursive))
155 for (relsrc, src) in results:
156 node = env.File(src)
157 src_nodes.append(node.srcnode())
158
159 # Get source files
160 if scan == 1 or scan == 2:
161 files = _get_files(env, source.srcnode().abspath, exclude, glob, recursive)
162 for (relsrc, src) in files:
163 node = env.File(src)
164 if not node in src_nodes:
165 results.append( (relsrc, src) )
166
167 return results
168
169
170 def InstallFiles(env, target, source, exclude=None, glob=None, recursive=True, scan=2):
171 """
172 InstallFiles pseudo-builder
173
174 target - target directory to install to
175 source - source file or directory to scan
176 exclude - a list of patterns to exclude in files and directories
177 glob - a list of patterns to include in files
178 recursive - scan directories recursively
179 scan - 0=scan built nodes, 1=scan source files, 2=both
180
181 All argument except target and source should be used as keyword arguments
182 """
183
184 # Information
185 if exclude:
186 exclude = Flatten(exclude)
187 else:
188 exclude = []
189 exclude.extend(env['INSTALLFILES_EXCLUDES'])
190
191 if glob:
192 glob = Flatten(glob)
193 else:
194 glob = []
195
196 # Flatten source/target
197 target = Flatten(target)
198 source = Flatten(source)
199
200 if len(target) != len(source):
201 if len(target) == 1:
202 # If only one target, assume it is for all the sources
203 target = target * len(source)
204 else:
205 raise SCons.Errors.UserError('InstallFiles expects only one target directory or one for each source')
206
207 # Scan
208 files = []
209 for (t, s) in zip(target, source):
210 if not isinstance(t, SCons.Node.FS.Base):
211 t = env.Dir(t)
212 if not isinstance(s, SCons.Node.FS.Base):
213 s = env.Entry(s)
214
215 for (relsrc, src) in _get_both(env, s, exclude, glob, recursive, scan):
216 dest = os.path.normpath(os.path.join(t.abspath, relsrc))
217 files.append( (dest, src) )
218
219 # Install
220 results = []
221 for (dest, src) in files:
222 results.extend(env.InstallAs(dest, src))
223
224 # Done
225 return results
226
227
228 def InstallPackageAccum(env, name, target, source, exclude=None, glob=None, recursive=True, scan=2):
229 """
230 InstallPackageAccum accumulate files for a package name
231
232 name - the name of the package of files
233 target - relative target directory under the package directory, can be '.'
234 source - source file or directory to scan
235 exclude - a list of patterns to exclude in files and directories
236 glob - a list of patterns to include in files
237 recursive - scan directories recursively
238 scan - 0=scan built nodes, 1=scan source files, 2=both
239
240 All argument except target and source should be used as keyword arguments and
241 target should NOT be nodes but strings such as '.'
242 """
243
244 # Information
245 if exclude:
246 exclude = Flatten(exclude)
247 else:
248 exclude = []
249 exclude.extend(env['INSTALLFILES_EXCLUDES'])
250
251 if glob:
252 glob = Flatten(glob)
253 else:
254 glob = []
255
256 # Flatten target/source
257 target = Flatten(target)
258 source = Flatten(source)
259
260 if len(target) != len(source):
261 if len(target) == 1:
262 target = target * len(source)
263 else:
264 raise SCons.Errors.UserError('InstallPackageAccum expects only one target directory or one for each source')
265
266 # Scan
267 files = []
268 for (t, s) in zip(target, source):
269 t = env.subst(t)
270 if not isinstance(s, SCons.Node.FS.Base):
271 s = env.Entry(s)
272
273 for (relsrc, src) in _get_both(env, s, exclude, glob, recursive, scan):
274 dest = os.path.normpath(os.path.join(t, relsrc))
275 files.append( (dest, src) )
276
277 # Add package if needed
278 packages = env['INSTALLFILES_PACKAGES']
279 if not name in packages:
280 packages[name] = []
281 packages[name].extend(files)
282
283
284 def InstallPackage(env, target, name):
285 """
286 Install the files of a given package to a certain location
287
288 target - the directory to install the package to
289 name - the name of the package to install
290 """
291
292 # Flatten target/name
293 target = Flatten(target)
294 name = Flatten(name)
295
296 if len(target) != len(name):
297 if len(target) == 1:
298 target = target * len(name)
299 else:
300 raise SCons.Errors.UserError('InstallPackage expects only one target directory or one for each package')
301
302 # Install
303 results = []
304 packages = env['INSTALLFILES_PACKAGES']
305
306 for (t, n) in zip(target, name):
307 if not n in packages:
308 raise SCons.Errors.UserError('InstallPackage package name does not exist: ' + n)
309
310 for (relsrc, src) in packages[n]:
311 dest = os.path.normpath(os.path.join(t, relsrc))
312 results.extend(env.InstallAs(dest, src))
313
314 # Done
315 return results
316
317
318 def InstallExclude(env, *args):
319 env['INSTALLFILES_EXCLUDES'] = []
320
321 for i in args:
322 env['INSTALLFILES_EXCLUDES'].extend(Flatten(i))
323
324
325 # Register this with the environment
326 def TOOL_INSTALL(env):
327 env.AddMethod(InstallFiles)
328 env.AddMethod(InstallPackageAccum)
329 env.AddMethod(InstallPackage)
330 env.AddMethod(InstallExclude)
331
332 env['INSTALLFILES_EXCLUDES'] = []
333 env['INSTALLFILES_PACKAGES'] = {}
The old builder code that only scans source files:
1 # File: install.py
2 # Author: Brian A. Vanderburg II
3 # Purpose: An install builder to install subdirectories and files
4 # Copyright: This file is placed in the public domain.
5 ##############################################################################
6
7
8 # Requirements
9 ##############################################################################
10 import os
11 import fnmatch
12
13 import SCons
14 import SCons.Node.FS
15 from SCons.Script import *
16
17
18 # Install files code
19 ##############################################################################
20
21 def _is_excluded(name, exclude):
22 """
23 Determine if the name is to be excluded based on the exclusion list
24 """
25 if not exclude:
26 return False
27 return any( (fnmatch.fnmatchcase(name, i) for i in exclude) )
28
29
30 def _is_globbed(name, glob):
31 """
32 Determine if the name is globbed based on the glob list
33 """
34 if not glob:
35 return True
36 return any( (fnmatch.fnmatchcase(name, i) for i in glob) )
37
38
39 def _get_files(env, target, source, exclude, glob, recursive):
40 """
41 Find files that match the criteria.
42
43 target - target directory node
44 source - source file or directory node
45 exclude - additional exclusion masks in addition to default
46 glob - glob masks
47 recursive - scan recursively or not
48
49 Returns a list of 2-element tuples where the first element is
50 the destination file name, the second is the source file name.
51 """
52
53 # Source and target path
54 # abspath intentionally used instead of str()
55 spath = os.path.normpath(source.abspath)
56 tpath = os.path.normpath(target.abspath)
57
58 # Since we are scanning now, the directory must exist now,
59 # otherwise treat it like a file
60 if not os.path.isdir(spath):
61 return [ (os.path.join(tpath, os.path.basename(spath)), spath) ]
62
63 # Scan source files on disk
64 results = []
65 for(dir, dirs, files) in os.walk(spath):
66 # Target for any file in this directory
67 reldir = dir[len(spath):]
68 while reldir and reldir[0] == os.sep:
69 reldir = reldir[1:]
70
71 if reldir:
72 destdir = os.path.join(tpath, reldir)
73 else:
74 destdir = tpath
75
76 # Files
77 for i in files:
78 if _is_excluded(i, exclude):
79 continue
80 if not _is_globbed(i, glob):
81 continue
82
83 results.append( (os.path.join(destdir, i), os.path.join(dir, i)) )
84
85 # Directories
86 i = 0
87 while i < len(dirs):
88 if recursive and not _is_excluded(dirs[i], exclude):
89 i += 1
90 else:
91 del dirs[i]
92
93 return results
94
95 def InstallFiles(env, target, source, exclude=None, glob=None, recursive=True):
96 """
97 InstallFiles pseudo-builder
98 """
99
100 # Information
101 if exclude:
102 exclude = Flatten(exclude)
103 else:
104 exclude = []
105 exclude.extend(env.get('INSTALL_EXCLUDES', []))
106
107 if glob:
108 glob = Flatten(glob)
109 else:
110 glob = []
111
112 # Install
113 target = Flatten(target)
114 source = Flatten(source)
115
116 if len(target) != len(source):
117 if len(target) == 1:
118 # If only one target, assume it is for all the sources
119 target = target * len(source)
120 else:
121 raise SCons.Errors.UserError('InstallFiles expects only one target directory or one for each source')
122
123 results = []
124 for (t, s) in zip(target, source):
125 if not isinstance(t, SCons.Node.FS.Base):
126 t = env.Dir(t)
127 if not isinstance(s, SCons.Node.FS.Base):
128 s = env.Entry(s)
129
130 files = _get_files(env, t, s, exclude, glob, recursive)
131 for (dest, src) in files:
132 results.extend(env.InstallAs(dest, src))
133
134 # Success
135 return results
136
137
138 def InstallExclude(env, *args):
139 env['INSTALL_EXCLUDES'] = []
140
141 for i in args:
142 env['INSTALL_EXCLUDES'].extend(Flatten(i))
143
144
145 # Register this with the environment
146 def TOOL_INSTALL(env):
147 env.AddMethod(InstallFiles)
148 env.AddMethod(InstallExclude)
