waf: Put private libraries in a separate directory when building.
[samba.git] / buildtools / wafsamba / samba_utils.py
1 # a waf tool to add autoconf-like macros to the configure section
2 # and for SAMBA_ macros for building libraries, binaries etc
3
4 import Build, os, sys, Options, Utils, Task, re, fnmatch, Logs
5 from TaskGen import feature, before
6 from Configure import conf
7 from Logs import debug
8 import shlex
9
10 # TODO: make this a --option
11 LIB_PATH="shared"
12
13
14 # sigh, python octal constants are a mess
15 MODE_644 = int('644', 8)
16 MODE_755 = int('755', 8)
17
18 @conf
19 def SET_TARGET_TYPE(ctx, target, value):
20     '''set the target type of a target'''
21     cache = LOCAL_CACHE(ctx, 'TARGET_TYPE')
22     if target in cache and cache[target] != 'EMPTY':
23         Logs.error("ERROR: Target '%s' in directory %s re-defined as %s - was %s" % (target, ctx.curdir, value, cache[target]))
24         sys.exit(1)
25     LOCAL_CACHE_SET(ctx, 'TARGET_TYPE', target, value)
26     debug("task_gen: Target '%s' created of type '%s' in %s" % (target, value, ctx.curdir))
27     return True
28
29
30 def GET_TARGET_TYPE(ctx, target):
31     '''get target type from cache'''
32     cache = LOCAL_CACHE(ctx, 'TARGET_TYPE')
33     if not target in cache:
34         return None
35     return cache[target]
36
37
38 ######################################################
39 # this is used as a decorator to make functions only
40 # run once. Based on the idea from
41 # http://stackoverflow.com/questions/815110/is-there-a-decorator-to-simply-cache-function-return-values
42 runonce_ret = {}
43 def runonce(function):
44     def runonce_wrapper(*args):
45         if args in runonce_ret:
46             return runonce_ret[args]
47         else:
48             ret = function(*args)
49             runonce_ret[args] = ret
50             return ret
51     return runonce_wrapper
52
53
54 def ADD_LD_LIBRARY_PATH(path):
55     '''add something to LD_LIBRARY_PATH'''
56     if 'LD_LIBRARY_PATH' in os.environ:
57         oldpath = os.environ['LD_LIBRARY_PATH']
58     else:
59         oldpath = ''
60     newpath = oldpath.split(':')
61     if not path in newpath:
62         newpath.append(path)
63         os.environ['LD_LIBRARY_PATH'] = ':'.join(newpath)
64
65
66 def install_rpath(bld):
67     '''the rpath value for installation'''
68     bld.env['RPATH'] = []
69     ret = set()
70     if bld.env.RPATH_ON_INSTALL:
71         ret.add(bld.EXPAND_VARIABLES(bld.env.LIBDIR))
72     if bld.env.RPATH_ON_INSTALL_PRIVATE:
73         ret.add(bld.EXPAND_VARIABLES(bld.env.PRIVATELIBDIR))
74     return list(ret)
75
76
77 def build_rpath(bld):
78     '''the rpath value for build'''
79     rpaths = [os.path.normpath('%s/%s' % (bld.env.BUILD_DIRECTORY, d)) for d in ("shared", "shared/private")]
80     bld.env['RPATH'] = []
81     if bld.env.RPATH_ON_BUILD:
82         return rpaths
83     for rpath in rpaths:
84         ADD_LD_LIBRARY_PATH(rpath)
85     return []
86
87
88 @conf
89 def LOCAL_CACHE(ctx, name):
90     '''return a named build cache dictionary, used to store
91        state inside other functions'''
92     if name in ctx.env:
93         return ctx.env[name]
94     ctx.env[name] = {}
95     return ctx.env[name]
96
97
98 @conf
99 def LOCAL_CACHE_SET(ctx, cachename, key, value):
100     '''set a value in a local cache'''
101     cache = LOCAL_CACHE(ctx, cachename)
102     cache[key] = value
103
104
105 @conf
106 def ASSERT(ctx, expression, msg):
107     '''a build assert call'''
108     if not expression:
109         raise Utils.WafError("ERROR: %s\n" % msg)
110 Build.BuildContext.ASSERT = ASSERT
111
112
113 def SUBDIR(bld, subdir, list):
114     '''create a list of files by pre-pending each with a subdir name'''
115     ret = ''
116     for l in TO_LIST(list):
117         ret = ret + os.path.normpath(os.path.join(subdir, l)) + ' '
118     return ret
119 Build.BuildContext.SUBDIR = SUBDIR
120
121
122 def dict_concat(d1, d2):
123     '''concatenate two dictionaries d1 += d2'''
124     for t in d2:
125         if t not in d1:
126             d1[t] = d2[t]
127
128
129 def exec_command(self, cmd, **kw):
130     '''this overrides the 'waf -v' debug output to be in a nice
131     unix like format instead of a python list.
132     Thanks to ita on #waf for this'''
133     import Utils, Logs
134     _cmd = cmd
135     if isinstance(cmd, list):
136         _cmd = ' '.join(cmd)
137     debug('runner: %s' % _cmd)
138     if self.log:
139         self.log.write('%s\n' % cmd)
140         kw['log'] = self.log
141     try:
142         if not kw.get('cwd', None):
143             kw['cwd'] = self.cwd
144     except AttributeError:
145         self.cwd = kw['cwd'] = self.bldnode.abspath()
146     return Utils.exec_command(cmd, **kw)
147 Build.BuildContext.exec_command = exec_command
148
149
150 def ADD_COMMAND(opt, name, function):
151     '''add a new top level command to waf'''
152     Utils.g_module.__dict__[name] = function
153     opt.name = function
154 Options.Handler.ADD_COMMAND = ADD_COMMAND
155
156
157 @feature('cc', 'cshlib', 'cprogram')
158 @before('apply_core','exec_rule')
159 def process_depends_on(self):
160     '''The new depends_on attribute for build rules
161        allow us to specify a dependency on output from
162        a source generation rule'''
163     if getattr(self , 'depends_on', None):
164         lst = self.to_list(self.depends_on)
165         for x in lst:
166             y = self.bld.name_to_obj(x, self.env)
167             self.bld.ASSERT(y is not None, "Failed to find dependency %s of %s" % (x, self.name))
168             y.post()
169             if getattr(y, 'more_includes', None):
170                   self.includes += " " + y.more_includes
171
172
173 os_path_relpath = getattr(os.path, 'relpath', None)
174 if os_path_relpath is None:
175     # Python < 2.6 does not have os.path.relpath, provide a replacement
176     # (imported from Python2.6.5~rc2)
177     def os_path_relpath(path, start):
178         """Return a relative version of a path"""
179         start_list = os.path.abspath(start).split("/")
180         path_list = os.path.abspath(path).split("/")
181
182         # Work out how much of the filepath is shared by start and path.
183         i = len(os.path.commonprefix([start_list, path_list]))
184
185         rel_list = ['..'] * (len(start_list)-i) + path_list[i:]
186         if not rel_list:
187             return start
188         return os.path.join(*rel_list)
189
190
191 def unique_list(seq):
192     '''return a uniquified list in the same order as the existing list'''
193     seen = {}
194     result = []
195     for item in seq:
196         if item in seen: continue
197         seen[item] = True
198         result.append(item)
199     return result
200
201
202 def TO_LIST(str, delimiter=None):
203     '''Split a list, preserving quoted strings and existing lists'''
204     if str is None:
205         return []
206     if isinstance(str, list):
207         return str
208     lst = str.split(delimiter)
209     # the string may have had quotes in it, now we
210     # check if we did have quotes, and use the slower shlex
211     # if we need to
212     for e in lst:
213         if e[0] == '"':
214             return shlex.split(str)
215     return lst
216
217
218 def subst_vars_error(string, env):
219     '''substitute vars, throw an error if a variable is not defined'''
220     lst = re.split('(\$\{\w+\})', string)
221     out = []
222     for v in lst:
223         if re.match('\$\{\w+\}', v):
224             vname = v[2:-1]
225             if not vname in env:
226                 Logs.error("Failed to find variable %s in %s" % (vname, string))
227                 sys.exit(1)
228             v = env[vname]
229         out.append(v)
230     return ''.join(out)
231
232
233 @conf
234 def SUBST_ENV_VAR(ctx, varname):
235     '''Substitute an environment variable for any embedded variables'''
236     return subst_vars_error(ctx.env[varname], ctx.env)
237 Build.BuildContext.SUBST_ENV_VAR = SUBST_ENV_VAR
238
239
240 def ENFORCE_GROUP_ORDERING(bld):
241     '''enforce group ordering for the project. This
242        makes the group ordering apply only when you specify
243        a target with --target'''
244     if Options.options.compile_targets:
245         @feature('*')
246         @before('exec_rule', 'apply_core', 'collect')
247         def force_previous_groups(self):
248             if getattr(self.bld, 'enforced_group_ordering', False) == True:
249                 return
250             self.bld.enforced_group_ordering = True
251
252             def group_name(g):
253                 tm = self.bld.task_manager
254                 return [x for x in tm.groups_names if id(tm.groups_names[x]) == id(g)][0]
255
256             my_id = id(self)
257             bld = self.bld
258             stop = None
259             for g in bld.task_manager.groups:
260                 for t in g.tasks_gen:
261                     if id(t) == my_id:
262                         stop = id(g)
263                         debug('group: Forcing up to group %s for target %s',
264                               group_name(g), self.name or self.target)
265                         break
266                 if stop != None:
267                     break
268             if stop is None:
269                 return
270
271             for i in xrange(len(bld.task_manager.groups)):
272                 g = bld.task_manager.groups[i]
273                 bld.task_manager.current_group = i
274                 if id(g) == stop:
275                     break
276                 debug('group: Forcing group %s', group_name(g))
277                 for t in g.tasks_gen:
278                     if not getattr(t, 'forced_groups', False):
279                         debug('group: Posting %s', t.name or t.target)
280                         t.forced_groups = True
281                         t.post()
282 Build.BuildContext.ENFORCE_GROUP_ORDERING = ENFORCE_GROUP_ORDERING
283
284
285 def recursive_dirlist(dir, relbase, pattern=None):
286     '''recursive directory list'''
287     ret = []
288     for f in os.listdir(dir):
289         f2 = dir + '/' + f
290         if os.path.isdir(f2):
291             ret.extend(recursive_dirlist(f2, relbase))
292         else:
293             if pattern and not fnmatch.fnmatch(f, pattern):
294                 continue
295             ret.append(os_path_relpath(f2, relbase))
296     return ret
297
298
299 def mkdir_p(dir):
300     '''like mkdir -p'''
301     if os.path.isdir(dir):
302         return
303     mkdir_p(os.path.dirname(dir))
304     os.mkdir(dir)
305
306
307 def SUBST_VARS_RECURSIVE(string, env):
308     '''recursively expand variables'''
309     if string is None:
310         return string
311     limit=100
312     while (string.find('${') != -1 and limit > 0):
313         string = subst_vars_error(string, env)
314         limit -= 1
315     return string
316
317
318 @conf
319 def EXPAND_VARIABLES(ctx, varstr, vars=None):
320     '''expand variables from a user supplied dictionary
321
322     This is most useful when you pass vars=locals() to expand
323     all your local variables in strings
324     '''
325
326     if isinstance(varstr, list):
327         ret = []
328         for s in varstr:
329             ret.append(EXPAND_VARIABLES(ctx, s, vars=vars))
330         return ret
331
332     import Environment
333     env = Environment.Environment()
334     ret = varstr
335     # substitute on user supplied dict if avaiilable
336     if vars is not None:
337         for v in vars.keys():
338             env[v] = vars[v]
339         ret = SUBST_VARS_RECURSIVE(ret, env)
340
341     # if anything left, subst on the environment as well
342     if ret.find('${') != -1:
343         ret = SUBST_VARS_RECURSIVE(ret, ctx.env)
344     # make sure there is nothing left. Also check for the common
345     # typo of $( instead of ${
346     if ret.find('${') != -1 or ret.find('$(') != -1:
347         Logs.error('Failed to substitute all variables in varstr=%s' % ret)
348         sys.exit(1)
349     return ret
350 Build.BuildContext.EXPAND_VARIABLES = EXPAND_VARIABLES
351
352
353 def RUN_COMMAND(cmd,
354                 env=None,
355                 shell=False):
356     '''run a external command, return exit code or signal'''
357     if env:
358         cmd = SUBST_VARS_RECURSIVE(cmd, env)
359
360     status = os.system(cmd)
361     if os.WIFEXITED(status):
362         return os.WEXITSTATUS(status)
363     if os.WIFSIGNALED(status):
364         return - os.WTERMSIG(status)
365     Logs.error("Unknown exit reason %d for command: %s" (status, cmd))
366     return -1
367
368
369 # make sure we have md5. some systems don't have it
370 try:
371     from hashlib import md5
372 except:
373     try:
374         import md5
375     except:
376         import Constants
377         Constants.SIG_NIL = hash('abcd')
378         class replace_md5(object):
379             def __init__(self):
380                 self.val = None
381             def update(self, val):
382                 self.val = hash((self.val, val))
383             def digest(self):
384                 return str(self.val)
385             def hexdigest(self):
386                 return self.digest().encode('hex')
387         def replace_h_file(filename):
388             f = open(filename, 'rb')
389             m = replace_md5()
390             while (filename):
391                 filename = f.read(100000)
392                 m.update(filename)
393             f.close()
394             return m.digest()
395         Utils.md5 = replace_md5
396         Task.md5 = replace_md5
397         Utils.h_file = replace_h_file
398
399
400 def LOAD_ENVIRONMENT():
401     '''load the configuration environment, allowing access to env vars
402        from new commands'''
403     import Environment
404     env = Environment.Environment()
405     try:
406         env.load('.lock-wscript')
407         env.load(env.blddir + '/c4che/default.cache.py')
408     except:
409         pass
410     return env
411
412
413 def IS_NEWER(bld, file1, file2):
414     '''return True if file1 is newer than file2'''
415     t1 = os.stat(os.path.join(bld.curdir, file1)).st_mtime
416     t2 = os.stat(os.path.join(bld.curdir, file2)).st_mtime
417     return t1 > t2
418 Build.BuildContext.IS_NEWER = IS_NEWER
419
420
421 @conf
422 def RECURSE(ctx, directory):
423     '''recurse into a directory, relative to the curdir or top level'''
424     try:
425         visited_dirs = ctx.visited_dirs
426     except:
427         visited_dirs = ctx.visited_dirs = set()
428     d = os.path.join(ctx.curdir, directory)
429     if os.path.exists(d):
430         abspath = os.path.abspath(d)
431     else:
432         abspath = os.path.abspath(os.path.join(Utils.g_module.srcdir, directory))
433     ctxclass = ctx.__class__.__name__
434     key = ctxclass + ':' + abspath
435     if key in visited_dirs:
436         # already done it
437         return
438     visited_dirs.add(key)
439     relpath = os_path_relpath(abspath, ctx.curdir)
440     if ctxclass == 'Handler':
441         return ctx.sub_options(relpath)
442     if ctxclass == 'ConfigurationContext':
443         return ctx.sub_config(relpath)
444     if ctxclass == 'BuildContext':
445         return ctx.add_subdirs(relpath)
446     Logs.error('Unknown RECURSE context class', ctxclass)
447     raise
448 Options.Handler.RECURSE = RECURSE
449 Build.BuildContext.RECURSE = RECURSE
450
451
452 def CHECK_MAKEFLAGS(bld):
453     '''check for MAKEFLAGS environment variable in case we are being
454     called from a Makefile try to honor a few make command line flags'''
455     if not 'WAF_MAKE' in os.environ:
456         return
457     makeflags = os.environ.get('MAKEFLAGS')
458     jobs_set = False
459     # we need to use shlex.split to cope with the escaping of spaces
460     # in makeflags
461     for opt in shlex.split(makeflags):
462         # options can come either as -x or as x
463         if opt[0:2] == 'V=':
464             Options.options.verbose = Logs.verbose = int(opt[2:])
465             if Logs.verbose > 0:
466                 Logs.zones = ['runner']
467             if Logs.verbose > 2:
468                 Logs.zones = ['*']
469         elif opt[0].isupper() and opt.find('=') != -1:
470             loc = opt.find('=')
471             setattr(Options.options, opt[0:loc], opt[loc+1:])
472         elif opt[0] != '-':
473             for v in opt:
474                 if v == 'j':
475                     jobs_set = True
476                 elif v == 'k':
477                     Options.options.keep = True                
478         elif opt == '-j':
479             jobs_set = True
480         elif opt == '-k':
481             Options.options.keep = True                
482     if not jobs_set:
483         # default to one job
484         Options.options.jobs = 1
485             
486 Build.BuildContext.CHECK_MAKEFLAGS = CHECK_MAKEFLAGS
487
488 option_groups = {}
489
490 def option_group(opt, name):
491     '''find or create an option group'''
492     global option_groups
493     if name in option_groups:
494         return option_groups[name]
495     gr = opt.add_option_group(name)
496     option_groups[name] = gr
497     return gr
498 Options.Handler.option_group = option_group
499
500
501 def save_file(filename, contents, create_dir=False):
502     '''save data to a file'''
503     if create_dir:
504         mkdir_p(os.path.dirname(filename))
505     try:
506         f = open(filename, 'w')
507         f.write(contents)
508         f.close()
509     except:
510         return False
511     return True
512
513
514 def load_file(filename):
515     '''return contents of a file'''
516     try:
517         f = open(filename, 'r')
518         r = f.read()
519         f.close()
520     except:
521         return None
522     return r
523
524
525 def reconfigure(ctx):
526     '''rerun configure if necessary'''
527     import Configure, samba_wildcard, Scripting
528     if not os.path.exists(".lock-wscript"):
529         raise Utils.WafError('configure has not been run')
530     bld = samba_wildcard.fake_build_environment()
531     Configure.autoconfig = True
532     Scripting.check_configured(bld)
533
534
535 def map_shlib_extension(ctx, name, python=False):
536     '''map a filename with a shared library extension of .so to the real shlib name'''
537     if name is None:
538         return None
539     if name[-1:].isdigit():
540         # some libraries have specified versions in the wscript rule
541         return name
542     (root1, ext1) = os.path.splitext(name)
543     if python:
544         (root2, ext2) = os.path.splitext(ctx.env.pyext_PATTERN)
545     else:
546         (root2, ext2) = os.path.splitext(ctx.env.shlib_PATTERN)
547     return root1+ext2
548 Build.BuildContext.map_shlib_extension = map_shlib_extension
549
550
551 def make_libname(ctx, name, nolibprefix=False, version=None, python=False):
552     """make a library filename
553          Options:
554               nolibprefix: don't include the lib prefix
555               version    : add a version number
556               python     : if we should use python module name conventions"""
557
558     if python:
559         libname = ctx.env.pyext_PATTERN % name
560     else:
561         libname = ctx.env.shlib_PATTERN % name
562     if nolibprefix and libname[0:3] == 'lib':
563         libname = libname[3:]
564     if version:
565         if version[0] == '.':
566             version = version[1:]
567         (root, ext) = os.path.splitext(libname)
568         if ext == ".dylib":
569             # special case - version goes before the prefix
570             libname = "%s.%s%s" % (root, version, ext)
571         else:
572             libname = "%s%s.%s" % (root, ext, version)
573     return libname
574 Build.BuildContext.make_libname = make_libname
575
576
577 def get_tgt_list(bld):
578     '''return a list of build objects for samba'''
579
580     targets = LOCAL_CACHE(bld, 'TARGET_TYPE')
581
582     # build a list of task generators we are interested in
583     tgt_list = []
584     for tgt in targets:
585         type = targets[tgt]
586         if not type in ['SUBSYSTEM', 'MODULE', 'BINARY', 'LIBRARY', 'ASN1', 'PYTHON']:
587             continue
588         t = bld.name_to_obj(tgt, bld.env)
589         if t is None:
590             Logs.error("Target %s of type %s has no task generator" % (tgt, type))
591             sys.exit(1)
592         tgt_list.append(t)
593     return tgt_list