cfc4a25c802a8b271430e2f6afbe8f3b181e4d29
[scons.git] / test / scan-once.py
1 #!/usr/bin/env python
2 #
3 # __COPYRIGHT__
4 #
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:
12 #
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
15 #
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.
23 #
24
25 """
26 This test verifies that Scanners are called just once.
27
28 This is actually a shotgun marriage of two separate tests, the simple
29 test originally created for this, plus a more complicated test based
30 on a real-life bug report submitted by Scott Lystig Fritchie.  Both
31 have value: the simple test will be easier to debug if there are basic
32 scanning problems, while Scott's test has a lot of cool real-world
33 complexity that is valuable in its own right, including scanning of
34 generated .h files.
35
36 """
37
38 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
39
40 import os.path
41 import sys
42
43 import TestCmd
44 import TestSCons
45
46 test = TestSCons.TestSCons()
47
48 test.subdir('simple',
49             'SLF',
50             ['SLF', 'reftree'], ['SLF', 'reftree', 'include'],
51             ['SLF', 'src'], ['SLF', 'src', 'lib_geng'])
52
53 test.write('SConstruct', """\
54 SConscript('simple/SConscript')
55 SConscript('SLF/SConscript')
56 """)
57
58 test.write(['simple', 'SConscript'], r"""
59 import os.path
60
61 def scan(node, env, envkey, arg):
62     print 'XScanner: node =', os.path.split(str(node))[1]
63     return []
64
65 def exists_check(node):
66     return os.path.exists(str(node))
67
68 XScanner = Scanner(name = 'XScanner',
69                    function = scan,
70                    argument = None,
71                    scan_check = exists_check,
72                    skeys = ['.x'])
73
74 def echo(env, target, source):
75     t = os.path.split(str(target[0]))[1]
76     s = os.path.split(str(source[0]))[1]
77     print 'create %s from %s' % (t, s)
78
79 Echo = Builder(action = Action(echo, None),
80                src_suffix = '.x',
81                suffix = '.x')
82
83 env = Environment(BUILDERS = {'Echo':Echo}, SCANNERS = [XScanner])
84
85 f1 = env.Echo(source=['file1'], target=['file2'])
86 f2 = env.Echo(source=['file2'], target=['file3'])
87 f3 = env.Echo(source=['file3'], target=['file4'])
88 """)
89
90 test.write(['simple', 'file1.x'], 'simple/file1.x\n')
91
92 test.write(['SLF', 'SConscript'], """\
93 ###
94 ### QQQ !@#$!@#$!  I need to move the SConstruct file to be "above"
95 ### both the source and install dirs, or the install dependencies
96 ### don't seem to work well!  ARRGH!!!!
97 ###
98
99 experimenttop = "%s"
100
101 import os
102 import os.path
103 import string
104 import Mylib
105
106 BStaticLibMerge = Builder(generator = Mylib.Gen_StaticLibMerge)
107 builders = Environment().Dictionary('BUILDERS')
108 builders["StaticLibMerge"] = BStaticLibMerge
109
110 env = Environment(BUILDERS = builders)
111 e = env.Dictionary()    # Slightly easier to type
112
113 Scanned = {}
114
115 def write_out(file, dict):
116     keys = dict.keys()
117     keys.sort()
118     f = open(file, 'wb')
119     for k in keys:
120         file = os.path.split(k)[1]
121         f.write(file + ": " + str(dict[k]) + "\\n")
122     f.close()
123
124 import SCons.Scanner.C
125 c_scanner = SCons.Scanner.C.CScan()
126 def MyCScan(node, env, target):
127     deps = c_scanner(node, env, target)
128
129     global Scanned
130     n = str(node)
131     try:
132         Scanned[n] = Scanned[n] + 1
133     except KeyError:
134         Scanned[n] = 1
135     write_out('MyCScan.out', Scanned)
136
137     return deps
138 S_MyCScan = SCons.Scanner.Current(skeys = [".c", ".C", ".cxx", ".cpp", ".c++", ".cc",
139                              ".h", ".H", ".hxx", ".hpp", ".h++", ".hh"],
140                     function = MyCScan,
141                     recursive = 1)
142 # QQQ Yes, this is manner of fixing the SCANNERS list is fragile.
143 env["SCANNERS"] = [S_MyCScan] + env["SCANNERS"][1:]
144
145 global_env = env
146 e["GlobalEnv"] = global_env
147
148 e["REF_INCLUDE"] = os.path.join(experimenttop, "reftree", "include")
149 e["REF_LIB"] = os.path.join(experimenttop, "reftree", "lib")
150 e["EXPORT_INCLUDE"] = os.path.join(experimenttop, "export", "include")
151 e["EXPORT_LIB"] = os.path.join(experimenttop, "export", "lib")
152 e["INSTALL_BIN"] = os.path.join(experimenttop, "install", "bin")
153
154 build_dir = os.path.join(experimenttop, "tmp-bld-dir")
155 src_dir = os.path.join(experimenttop, "src")
156
157 env.Append(CPPPATH = [e["EXPORT_INCLUDE"]])
158 env.Append(CPPPATH = [e["REF_INCLUDE"]])
159 Mylib.AddLibDirs(env, "/via/Mylib.AddLibPath")
160 env.Append(LIBPATH = [e["EXPORT_LIB"]])
161 env.Append(LIBPATH = [e["REF_LIB"]])
162
163 Mylib.Subdirs(env, "src")
164 """ % test.workpath('SLF'))
165
166 test.write(['SLF', 'Mylib.py'], """\
167 import os
168 import string
169 import re
170 import SCons.Environment
171
172 def Subdirs(env, dirlist):
173     for file in _subconf_list(dirlist):
174         env.SConscript(file, "env")
175
176 def _subconf_list(dirlist):
177     return map(lambda x: os.path.join(x, "SConscript"), string.split(dirlist))
178
179 def StaticLibMergeMembers(local_env, libname, hackpath, files):
180     for file in string.split(files):
181         # QQQ Fix limits in grok'ed regexp
182         tmp = re.sub(".c$", ".o", file)
183         objname = re.sub(".cpp", ".o", tmp)
184         local_env.Object(target = objname, source = file)
185         e = 'local_env["GlobalEnv"].Append(%s = ["%s"])' % (libname, os.path.join(hackpath, objname))
186         exec(e)
187
188 def CreateMergedStaticLibrary(env, libname):
189     objpaths = env["GlobalEnv"][libname]
190     libname = "lib%s.a" % (libname)
191     env.StaticLibMerge(target = libname, source = objpaths)
192
193 # I put the main body of the generator code here to avoid
194 # namespace problems
195 def Gen_StaticLibMerge(source, target, env, for_signature):
196     target_string = ""
197     for t in target:
198         target_string = str(t)
199     subdir = os.path.dirname(target_string)
200     srclist = []
201     for src in source:
202         srclist.append(src)
203     return [["ar", "cq"] + target + srclist, ["ranlib"] + target]
204
205 def StaticLibrary(env, target, source):
206     env.StaticLibrary(target, string.split(source))
207
208 def SharedLibrary(env, target, source):
209     env.SharedLibrary(target, string.split(source))
210
211 def ExportHeader(env, headers):
212     env.Install(dir = env["EXPORT_INCLUDE"], source = string.split(headers))
213
214 def ExportLib(env, libs):
215     env.Install(dir = env["EXPORT_LIB"], source = string.split(libs))
216
217 def InstallBin(env, bins):
218     env.Install(dir = env["INSTALL_BIN"], source = string.split(bins))
219
220 def Program(env, target, source):
221     env.Program(target, string.split(source))
222
223 def AddCFlags(env, str):
224     env.Append(CPPFLAGS = " " + str)
225
226 # QQQ Synonym needed?
227 #def AddCFLAGS(env, str):
228 #    AddCFlags(env, str)
229
230 def AddIncludeDirs(env, str):
231     env.Append(CPPPATH = string.split(str))
232
233 def AddLibs(env, str):
234     env.Append(LIBS = string.split(str))
235
236 def AddLibDirs(env, str):
237     env.Append(LIBPATH = string.split(str))
238
239 """)
240
241 test.write(['SLF', 'reftree', 'include', 'lib_a.h'], """\
242 char *a_letter(void);
243 """)
244
245 test.write(['SLF', 'reftree', 'include', 'lib_b.h'], """\
246 char *b_letter(void);
247 """)
248
249 test.write(['SLF', 'reftree', 'include', 'lib_ja.h'], """\
250 char *j_letter_a(void);
251 """)
252
253 test.write(['SLF', 'reftree', 'include', 'lib_jb.h.intentionally-moved'], """\
254 char *j_letter_b(void);
255 """)
256
257 test.write(['SLF', 'src', 'SConscript'], """\
258 # --- Begin SConscript boilerplate ---
259 import Mylib
260 Import("env")
261
262 #env = env.Copy()    # Yes, clobber intentionally
263 #Make environment changes, such as: Mylib.AddCFlags(env, "-g -D_TEST")
264 #Mylib.Subdirs(env, "lib_a lib_b lib_mergej prog_x")
265 Mylib.Subdirs(env, "lib_geng")
266
267 env = env.Copy()    # Yes, clobber intentionally
268 # --- End SConscript boilerplate ---
269
270 """)
271
272 test.write(['SLF', 'src', 'lib_geng', 'SConscript'], """\
273 # --- Begin SConscript boilerplate ---
274 import string
275 import sys
276 import Mylib
277 Import("env")
278
279 #env = env.Copy()    # Yes, clobber intentionally
280 #Make environment changes, such as: Mylib.AddCFlags(env, "-g -D_TEST")
281 #Mylib.Subdirs(env, "foo_dir")
282
283 env = env.Copy()    # Yes, clobber intentionally
284 # --- End SConscript boilerplate ---
285
286 Mylib.AddCFlags(env, "-DGOOFY_DEMO")
287 Mylib.AddIncludeDirs(env, ".")
288
289 # Not part of SLF's original stuff: On Win32, it's import to use the
290 # original test environment when we invoke SCons recursively.
291 import os
292 recurse_env = env.Copy()
293 recurse_env["ENV"] = os.environ
294
295 # Icky code to set up process environment for "make"
296 # I really ought to drop this into Mylib....
297
298 fromdict = env.Dictionary()
299 todict = env["ENV"]
300 import SCons.Util
301 import re
302 for k in fromdict.keys():
303     if k != "ENV" and k != "SCANNERS" and k != "CFLAGS" and k != "CXXFLAGS" \
304     and not SCons.Util.is_Dict(fromdict[k]):
305         # The next line can fail on some systems because it would try to
306         # do env.subst on:
307         #       $RMIC $RMICFLAGS -d ${TARGET.attributes.java_lookupdir} ...
308         # When $TARGET is None, so $TARGET.attributes would throw an
309         # exception.
310         f = fromdict[k]
311         if SCons.Util.is_String(f) and string.find(f, "TARGET") == -1:
312              todict[k] = env.subst(f)
313 todict["CFLAGS"] = fromdict["CPPFLAGS"] + " " + \
314     string.join(map(lambda x: "-I" + x, env["CPPPATH"])) + " " + \
315     string.join(map(lambda x: "-L" + x, env["LIBPATH"])) 
316 todict["CXXFLAGS"] = todict["CFLAGS"]
317
318 generated_hdrs = "libg_gx.h libg_gy.h libg_gz.h"
319 static_hdrs = "libg_w.h"
320 #exported_hdrs = generated_hdrs + " " + static_hdrs
321 exported_hdrs = static_hdrs
322 lib_name = "g"
323 lib_fullname = "libg.a"
324 lib_srcs = string.split("libg_1.c libg_2.c libg_3.c")
325 import re
326 lib_objs = map(lambda x: re.sub("\.c$", ".o", x), lib_srcs)
327
328 Mylib.ExportHeader(env, exported_hdrs)
329 Mylib.ExportLib(env, lib_fullname)
330
331 # The following were the original commands from SLF, making use of
332 # a shell script and a Makefile to build the library.  These have
333 # been preserved, commented out below, but in order to make this
334 # test portable, we've replaced them with a Python script and a
335 # recursive invocation of SCons (!).
336 #cmd_both = "cd %s ; make generated ; make" % Dir(".")
337 #cmd_generated = "cd %s ; sh MAKE-HEADER.sh" % Dir(".")
338 #cmd_justlib = "cd %s ; make" % Dir(".")
339
340 _ws = re.compile('\s')
341
342 def escape(s):
343     if _ws.search(s):
344         s = '"' + s + '"'
345     return s
346
347 cmd_generated = "%s $SOURCE" % (escape(sys.executable),)
348 cmd_justlib = "%s %s -C ${SOURCES[0].dir}" % ((sys.executable),
349                                               escape(sys.argv[0]))
350
351 ##### Deps appear correct ... but wacky scanning?
352 # Why?
353 #
354 # SCons bug??
355
356 env.Command(string.split(generated_hdrs),
357             ["MAKE-HEADER.py"],
358             cmd_generated)
359 recurse_env.Command([lib_fullname] + lib_objs,
360                     lib_srcs + string.split(generated_hdrs + " " + static_hdrs),
361                     cmd_justlib) 
362 """)
363
364 test.write(['SLF', 'src', 'lib_geng', 'MAKE-HEADER.py'], """\
365 #!/usr/bin/env python
366
367 import os
368 import os.path
369 import sys
370
371 # chdir to the directory in which this script lives
372 os.chdir(os.path.split(sys.argv[0])[0])
373
374 for h in ['libg_gx.h', 'libg_gy.h', 'libg_gz.h']:
375     open(h, 'w').write('')
376 """)
377
378 test.write(['SLF', 'src', 'lib_geng', 'SConstruct'], """\
379 env = Environment(CPPPATH = ".")
380 l = env.StaticLibrary("g", Split("libg_1.c libg_2.c libg_3.c"))
381 Default(l)
382 """)
383
384 # These were the original shell script and Makefile from SLF's original
385 # bug report.  We're not using them--in order to make this script as
386 # portable as possible, we're using a Python script and a recursive
387 # invocation of SCons--but we're preserving them here for history.
388 #test.write(['SLF', 'src', 'lib_geng', 'MAKE-HEADER.sh'], """\
389 ##!/bin/sh
390 #
391 #exec touch $*
392 #""")
393 #
394 #test.write(['SLF', 'src', 'lib_geng', 'Makefile'], """\
395 #all: libg.a
396 #
397 #GEN_HDRS = libg_gx.h libg_gy.h libg_gz.h
398 #STATIC_HDRS = libg_w.h
399 #
400 #$(GEN_HDRS): generated
401 #
402 #generated: MAKE-HEADER.sh
403 #       sh ./MAKE-HEADER.sh $(GEN_HDRS)
404 #
405 #libg.a: libg_1.o libg_2.o libg_3.o
406 #       ar r libg.a libg_1.o libg_2.o libg_3.o
407 #
408 #libg_1.c: $(STATIC_HDRS) $(GEN_HDRS)
409 #libg_2.c: $(STATIC_HDRS) $(GEN_HDRS)
410 #libg_3.c: $(STATIC_HDRS) $(GEN_HDRS)
411 #
412 #clean:
413 #       -rm -f $(GEN_HDRS)
414 #       -rm -f libg.a *.o core core.*
415 #""")
416
417 test.write(['SLF', 'src', 'lib_geng', 'libg_w.h'], """\
418 """)
419
420 test.write(['SLF', 'src', 'lib_geng', 'libg_1.c'], """\
421 #include <libg_w.h>
422 #include <libg_gx.h>
423
424 int g_1()
425 {
426     return 1;
427 }
428 """)
429
430 test.write(['SLF', 'src', 'lib_geng', 'libg_2.c'], """\
431 #include <libg_w.h>
432 #include <libg_gx.h> 
433 #include <libg_gy.h>
434 #include <libg_gz.h>
435
436 int g_2()
437 {
438         return 2;
439 }
440 """)
441
442 test.write(['SLF', 'src', 'lib_geng', 'libg_3.c'], """\
443 #include <libg_w.h>
444 #include <libg_gx.h>
445
446 int g_3()
447 {
448     return 3;
449 }
450 """)
451
452 test.run(arguments = 'simple',
453          stdout = test.wrap_stdout("""\
454 XScanner: node = file1.x
455 create file2.x from file1.x
456 create file3.x from file2.x
457 create file4.x from file3.x
458 """))
459
460 test.write(['simple', 'file2.x'], 'simple/file2.x\n')
461
462 test.run(arguments = 'simple',
463          stdout = test.wrap_stdout("""\
464 XScanner: node = file1.x
465 XScanner: node = file2.x
466 create file3.x from file2.x
467 create file4.x from file3.x
468 """))
469
470 test.write(['simple', 'file3.x'], 'simple/file3.x\n')
471
472 test.run(arguments = 'simple',
473          stdout = test.wrap_stdout("""\
474 XScanner: node = file1.x
475 XScanner: node = file2.x
476 XScanner: node = file3.x
477 create file4.x from file3.x
478 """))
479
480 test.run(arguments = 'SLF')
481
482 # XXX Note that the generated .h files still get scanned twice,
483 # once before they're generated and once after.  That's the
484 # next thing to fix here.
485 test.fail_test(test.read("MyCScan.out", "rb") != """\
486 libg_1.c: 1
487 libg_2.c: 1
488 libg_3.c: 1
489 libg_gx.h: 1
490 libg_gy.h: 1
491 libg_gz.h: 1
492 libg_w.h: 1
493 """)
494
495 test.pass_test()