configure already sets TMPDIR to mixed path
[LibreOffice.git] / bin / update_pch
blobfcac5b0e4711525d2e972885116800d4adaffb0f
1 #! /usr/bin/env python3
2 # -*- Mode: python; tab-width: 4; indent-tabs-mode: t -*-
4 # This file is part of the LibreOffice project.
6 # This Source Code Form is subject to the terms of the Mozilla Public
7 # License, v. 2.0. If a copy of the MPL was not distributed with this
8 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
11 """
12 This script generates precompiled headers for a given
13 module and library.
15 Given a gmake makefile that belongs to some LO module:
16 1) Process the makefile to find source files (process_makefile).
17 2) For every source file, find all includes (process_source).
18 3) Uncommon and rare includes are filtered (remove_rare).
19 4) Conflicting headers are excluded (filter_ignore).
20 5) Local files to the source are excluded (Filter_Local).
21 6) Fixup missing headers that sources expect (fixup).
22 7) The resulting includes are sorted by category (sort_by_category).
23 8) The pch file is generated (generate).
24 """
26 import sys
27 import re
28 import os
29 import unittest
30 import glob
32 CUTOFF = 1
33 EXCLUDE_MODULE = False
34 EXCLUDE_LOCAL = False
35 EXCLUDE_SYSTEM = True
36 SILENT = False
37 WORKDIR = 'workdir'
39 # System includes: oox, sal, sd, svl, vcl
41 INCLUDE = False
42 EXCLUDE = True
43 DEFAULTS = \
45 #    module.library : (min, system, module, local), best time
46     'accessibility.acc'                 : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   7.8
47     'basctl.basctl'                     : ( 3, EXCLUDE, INCLUDE, EXCLUDE), #  11.9
48     'basegfx.basegfx'                   : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   3.8
49     'basic.sb'                          : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #  10.7
50     'chart2.chartcontroller'            : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  18.4
51     'chart2.chartcore'                  : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #  22.5
52     'comphelper.comphelper'             : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   7.6
53     'configmgr.configmgr'               : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   6.0
54     'connectivity.ado'                  : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   6.4
55     'connectivity.calc'                 : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
56     'connectivity.dbase'                : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   5.2
57     'connectivity.dbpool2'              : ( 5, EXCLUDE, INCLUDE, EXCLUDE), #   3.0
58     'connectivity.dbtools'              : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   0.8
59     'connectivity.file'                 : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   5.1
60     'connectivity.firebird_sdbc'        : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   5.1
61     'connectivity.flat'                 : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.6
62     'connectivity.mysql'                : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #   3.4
63     'connectivity.odbc'                 : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   5.0
64     'connectivity.postgresql-sdbc-impl' : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   6.7
65     'cppcanvas.cppcanvas'               : (11, EXCLUDE, INCLUDE, INCLUDE), #   4.8
66     'cppuhelper.cppuhelper'             : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
67     'cui.cui'                           : ( 8, EXCLUDE, INCLUDE, EXCLUDE), #  19.7
68     'dbaccess.dba'                      : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  13.8
69     'dbaccess.dbaxml'                   : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   6.5
70     'dbaccess.dbu'                      : (12, EXCLUDE, EXCLUDE, EXCLUDE), #  23.6
71     'dbaccess.sdbt'                     : ( 1, EXCLUDE, INCLUDE, EXCLUDE), #   2.9
72     'desktop.deployment'                : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   6.1
73     'desktop.deploymentgui'             : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   5.7
74     'desktop.deploymentmisc'            : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   3.4
75     'desktop.sofficeapp'                : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   6.5
76     'docmodel.docmodel'                 : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   3.8
77     'drawinglayer.drawinglayer'         : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   7.4
78     'editeng.editeng'                   : ( 5, EXCLUDE, INCLUDE, EXCLUDE), #  13.0
79     'forms.frm'                         : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #  14.2
80     'framework.fwk'                     : ( 7, EXCLUDE, INCLUDE, INCLUDE), #  14.8
81     'hwpfilter.hwp'                     : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   6.0
82     'lotuswordpro.lwpft'                : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #  11.6
83     'oox.oox'                           : ( 6, EXCLUDE, EXCLUDE, INCLUDE), #  28.2
84     'package.package2'                  : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   4.5
85     'package.xstor'                     : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   3.8
86     'reportdesign.rpt'                  : ( 9, EXCLUDE, INCLUDE, INCLUDE), #   9.4
87     'reportdesign.rptui'                : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  13.1
88     'reportdesign.rptxml'               : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   7.6
89     'sal.sal'                           : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   4.2
90     'sc.sc'                             : (12, EXCLUDE, INCLUDE, INCLUDE), #  92.6
91     'sc.scfilt'                         : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #  39.9
92     'sc.scui'                           : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #  15.0
93     'sc.vbaobj'                         : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #  17.3
94     'sd.sd'                             : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #  47.4
95     'sd.sdui'                           : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   9.4
96     'sdext.PresentationMinimizer'       : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.1
97     'sdext.PresenterScreen'             : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   7.1
98     'sfx2.sfx'                          : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #  27.4
99     'slideshow.slideshow'               : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #  10.8
100     'sot.sot'                           : ( 5, EXCLUDE, EXCLUDE, INCLUDE), #   3.1
101     'starmath.sm'                       : ( 5, EXCLUDE, EXCLUDE, INCLUDE), #  10.9
102     'svgio.svgio'                       : ( 8, EXCLUDE, EXCLUDE, INCLUDE), #   4.3
103     'emfio.emfio'                       : ( 8, EXCLUDE, EXCLUDE, INCLUDE), #   4.3
104     'svl.svl'                           : ( 6, EXCLUDE, EXCLUDE, EXCLUDE), #   7.6
105     'svtools.svt'                       : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #  17.6
106     'svx.svx'                           : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #  20.7
107     'svx.svxcore'                       : ( 7, EXCLUDE, INCLUDE, EXCLUDE), #  37.0
108     'sw.msword'                         : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  22.4
109     'sw.sw'                             : ( 7, EXCLUDE, EXCLUDE, INCLUDE), # 129.6
110     'sw.swui'                           : ( 3, EXCLUDE, INCLUDE, INCLUDE), #  26.1
111     'sw.vbaswobj'                       : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  13.1
112     'sw.sw_writerfilter'                : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #  19.7/27.3
113     'tools.tl'                          : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #   4.2
114     'unotools.utl'                      : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
115     'unoxml.unoxml'                     : ( 1, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
116     'uui.uui'                           : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   4.9
117     'vbahelper.msforms'                 : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   5.2
118     'vbahelper.vbahelper'               : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
119     'vcl.vcl'                           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  35.7
120     'xmloff.xo'                         : ( 7, EXCLUDE, INCLUDE, INCLUDE), #  22.1
121     'xmloff.xof'                        : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #   4.4
122     'xmlscript.xmlscript'               : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #   3.6
123     'xmlsecurity.xmlsecurity'           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   5.1
124     'xmlsecurity.xsec_xmlsec'           : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.4
125     'xmlsecurity.xsec_gpg'              : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   ?
128 def remove_rare(raw, min_use=-1):
129     """ Remove headers not commonly included.
130         The minimum threshold is min_use.
131     """
132     # The minimum number of times a header
133     # must be included to be in the PCH.
134     min_use = min_use if min_use >= 0 else CUTOFF
136     out = []
137     if not raw or not len(raw):
138         return out
140     inc = sorted(raw)
141     last = inc[0]
142     count = 1
143     for x in range(1, len(inc)):
144         i = inc[x]
145         if i == last:
146             count += 1
147         else:
148             if count >= min_use:
149                 out.append(last)
150             last = i
151             count = 1
153     # Last group.
154     if count >= min_use:
155         out.append(last)
157     return out
159 def process_list(list, callable):
160     """ Given a list and callable
161         we pass each entry through
162         the callable and only add to
163         the output if not blank.
164     """
165     out = []
166     for i in list:
167         line = callable(i)
168         if line and len(line):
169             out.append(line)
170     return out
172 def find_files(path, recurse=True):
173     list = []
174     for root, dir, files in os.walk(path):
175         list += map(lambda x: os.path.join(root, x), files)
176     return list
178 def get_filename(line):
179     """ Strips the line from the
180         '#include' and angled brackets
181         and return the filename only.
182     """
183     if not len(line) or line[0] != '#':
184         return line
185     return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
187 def is_c_runtime(inc, root, module):
188     """ Heuristic-based detection of C/C++
189         runtime headers.
190         They are all-lowercase, with .h or
191         no extension, filename only.
192         Try to check that they are not LO headers.
193     """
194     inc = get_filename(inc)
196     if inc.endswith('.hxx') or inc.endswith('.hpp'):
197         return False
199     if inc.endswith('.h') and inc.startswith( 'config_' ):
200         return False
202     hasdot = False
203     for c in inc:
204         if c == '/':
205             return False
206         if c == '.' and not inc.endswith('.h'):
207             return False
208         if c == '.':
209             hasdot = True
210         if c.isupper():
211             return False
212     if not hasdot: # <memory> etc.
213         return True
215     if glob.glob(os.path.join(root, module, '**', inc), recursive=True):
216         return False;
218     return True
220 def sanitize(raw):
221     """ There are two forms of includes,
222         those with <> and "".
223         Technically, the difference is that
224         the compiler can use an internal
225         representation for an angled include,
226         such that it doesn't have to be a file.
227         For our purposes, there is no difference.
228         Here, we convert everything to angled.
229     """
230     if not raw or not len(raw):
231         return ''
232     raw = raw.strip()
233     if not len(raw):
234         return ''
235     return re.sub(r'(.*#include\s*)\"(.*)\"(.*)', r'#include <\2>', raw)
237 class Filter_Local(object):
238     """ Filter headers local to a module.
239         allow_public: allows include/module/file.hxx
240                       #include <module/file.hxx>
241         allow_module: allows module/inc/file.hxx
242                       #include <file.hxx>
243         allow_locals: allows module/source/file.hxx and
244                              module/source/inc/file.hxx
245                       #include <file.hxx>
246     """
247     def __init__(self, root, module, allow_public=True, allow_module=True, allow_locals=True):
248         self.root = root
249         self.module = module
250         self.allow_public = allow_public
251         self.allow_module = allow_module
252         self.allow_locals = allow_locals
253         self.public_prefix = '<' + self.module + '/'
255         all = find_files(os.path.join(root, module))
256         self.module_includes = []
257         self.locals = []
258         mod_prefix = module + '/inc/'
259         for i in all:
260             if mod_prefix in i:
261                 self.module_includes.append(i)
262             else:
263                 self.locals.append(i)
265     def is_public(self, line):
266         return self.public_prefix in line
268     def is_module(self, line):
269         """ Returns True if in module/inc/... """
270         filename = get_filename(line)
271         for i in self.module_includes:
272             if i.endswith(filename):
273                 return True
274         return False
276     def is_local(self, line):
277         """ Returns True if in module/source/... """
278         filename = get_filename(line)
279         for i in self.locals:
280             if i.endswith(filename):
281                 return True
282         return False
284     def is_external(self, line):
285         return is_c_runtime(line, self.root, self.module) and \
286                not self.is_public(line) and \
287                not self.is_module(line) and \
288                not self.is_local(line)
290     def find_local_file(self, line):
291         """ Finds the header file in the module dir,
292             but doesn't validate.
293         """
294         filename = get_filename(line)
295         for i in self.locals:
296             if i.endswith(filename):
297                 return i
298         for i in self.module_includes:
299             if i.endswith(filename):
300                 return i
301         return None
303     def proc(self, line):
304         assert line and len(line)
306         if line[0] == '#':
307             if not SILENT:
308                 sys.stderr.write('unhandled #include : {}\n'.format(line))
309             return ''
311         assert line[0] != '<' and line[0] != '#'
313         filename = get_filename(line)
315         # Local with relative path.
316         if filename.startswith('..'):
317             # Exclude for now as we don't have cxx path.
318             return ''
320         # Locals are included first (by the compiler).
321         if self.is_local(filename):
322             # Use only locals that are in some /inc/ directory (either in <module>/inc or
323             # somewhere under <module>/source/**/inc/, compilations use -I for these paths
324             # and headers elsewhere would not be found when compiling the PCH.
325             if not self.allow_locals:
326                 return ''
327             elif '/inc/' in filename:
328                 return filename
329             elif glob.glob(os.path.join(self.root, self.module, '**', 'inc', filename), recursive=True):
330                 return filename
331             else:
332                 return ''
334         # Module headers are next.
335         if self.is_module(filename):
336             return line if self.allow_module else ''
338         # Public headers are last.
339         if self.is_public(line):
340             return line if self.allow_public else ''
342         # Leave out potentially unrelated files local
343         # to some other module we can't include directly.
344         if '/' not in filename and not self.is_external(filename):
345             return ''
347         # Unfiltered.
348         return line
350 def filter_ignore(line, module):
351     """ Filters includes from known
352         problematic ones.
353         Expects sanitized input.
354     """
355     assert line and len(line)
357     # Always include files without extension.
358     if '.' not in line:
359         return line
361     # Extract filenames for ease of comparison.
362     line = get_filename(line)
364     # Filter out all files that are not normal headers.
365     if not line.endswith('.h') and \
366        not line.endswith('.hxx') and \
367        not line.endswith('.hpp') and \
368        not line.endswith('.hdl'):
369        return ''
371     ignore_list = [
372             'LibreOfficeKit/LibreOfficeKitEnums.h', # Needs special directives
373             'LibreOfficeKit/LibreOfficeKitTypes.h', # Needs special directives
374             'jerror.h',     # c++ unfriendly
375             'jpeglib.h',    # c++ unfriendly
376             'boost/spirit/include/classic_core.hpp' # depends on BOOST_SPIRIT_DEBUG
377         ]
379     if module == 'basic':
380         ignore_list += [
381             'basic/vbahelper.hxx',
382             ]
383     if module == 'connectivity':
384         ignore_list += [
385             'com/sun/star/beans/PropertyAttribute.hpp', # OPTIONAL defined via objbase.h
386             'com/sun/star/sdbcx/Privilege.hpp', # DELETE defined via objbase.h
387             'ado/*' , # some strange type conflict because of Window's adoctint.h
388             'adoint.h',
389             'adoctint.h',
390             ]
391     if module == 'sc':
392         ignore_list += [
393             'progress.hxx', # special directives
394             'scslots.hxx',  # special directives
395            ]
396     if module == 'sd':
397         ignore_list += [
398             'sdgslots.hxx', # special directives
399             'sdslots.hxx',  # special directives
400            ]
401     if module == 'sfx2':
402         ignore_list += [
403             'sfx2/recentdocsview.hxx', # Redefines ApplicationType defined in objidl.h
404             'sfx2/sidebar/Sidebar.hxx',
405             'sfx2/sidebar/UnoSidebar.hxx',
406             'sfxslots.hxx', # externally defined types
407             ]
408     if module == 'sot':
409         ignore_list += [
410             'sysformats.hxx',   # Windows headers
411             ]
412     if module == 'vcl':
413         ignore_list += [
414             'accmgr.hxx',   # redefines ImplAccelList
415             'image.h',
416             'jobset.h',
417             'opengl/gdiimpl.hxx',
418             'opengl/salbmp.hxx',
419             'openglgdiimpl',   # ReplaceTextA
420             'printdlg.hxx',
421             'salinst.hxx',  # GetDefaultPrinterA
422             'salprn.hxx',   # SetPrinterDataA
423             'vcl/jobset.hxx',
424             'vcl/oldprintadaptor.hxx',
425             'vcl/opengl/OpenGLContext.hxx',
426             'vcl/opengl/OpenGLHelper.hxx',  # Conflicts with X header on *ix
427             'vcl/print.hxx',
428             'vcl/prntypes.hxx', # redefines Orientation from filter/jpeg/Exif.hxx
429             'vcl/sysdata.hxx',
430             ]
431     if module == 'xmloff':
432         ignore_list += [
433             'SchXMLExport.hxx', # SchXMLAutoStylePoolP.hxx not found
434             'SchXMLImport.hxx', # enums redefined in draw\sdxmlimp_impl.hxx
435             'XMLEventImportHelper.hxx', # NameMap redefined in XMLEventExport.hxx
436             'xmloff/XMLEventExport.hxx', # enums redefined
437             ]
438     if module == 'xmlsecurity':
439         ignore_list += [
440             'xmlsec/*',
441             ]
442     if module == 'external/pdfium':
443         ignore_list += [
444             'third_party/freetype/include/pstables.h',
445             ]
446     if module == 'external/clucene':
447         ignore_list += [
448             '_bufferedstream.h',
449             '_condition.h',
450             '_gunichartables.h',
451             '_threads.h',
452             'error.h',
453             'CLucene/LuceneThreads.h',
454             'CLucene/config/_threads.h',
455             ]
456     if module == 'external/skia':
457         ignore_list += [
458             'skcms_internal.h',
459             'zlib.h', # causes crc32 conflict
460             'dirent.h', # unix-specific
461             'pthread.h',
462             'unistd.h',
463             'sys/stat.h',
464             'ft2build.h',
465             'fontconfig/fontconfig.h',
466             'GL/glx.h',
467             'src/Transform_inl.h',
468             'src/c/sk_c_from_to.h',
469             'src/c/sk_types_priv.h',
470             'src/core/SkBlitBWMaskTemplate.h',
471             'src/sfnt/SkSFNTHeader.h',
472             'src/opts/',
473             'src/core/SkCubicSolver.h',
474             'src/sksl/SkSLCPP.h',
475             'src/gpu/vk/GrVkAMDMemoryAllocator.h',
476             'src/gpu/GrUtil.h',
477             'src/sksl/', # conflict between SkSL::Expression and SkSL::dsl::Expression
478             'include/sksl/',
479             'src/gpu/vk/',
480             'include/gpu/vk'
481             ]
482     if module == 'external/zxing':
483         ignore_list += [
484             'rss/ODRSSExpandedBinaryDecoder.h'
485             ]
487     for i in ignore_list:
488         if line.startswith(i):
489             return ''
490         if i[0] == '*' and line.endswith(i[1:]):
491             return ''
492         if i[-1] == '*' and line.startswith(i[:-1]):
493             return ''
495     return line
497 def fixup(includes, module):
498     """ Here we add any headers
499         necessary in the pch.
500         These could be known to be very
501         common but for technical reasons
502         left out of the pch by this generator.
503         Or, they could be missing from the
504         source files where they are used
505         (probably because they had been
506         in the old pch, they were missed).
507         Also, these could be headers
508         that make the build faster but
509         aren't added automatically.
510     """
511     fixes = []
512     def append(inc):
513         # Add a space to exclude from
514         # ignore bisecting.
515         line = ' #include <{}>'.format(inc)
516         try:
517             i = fixes.index(inc)
518             fixes[i] = inc
519         except:
520             fixes.append(inc)
522     append('sal/config.h')
524     if module == 'basctl':
525         if 'basslots.hxx' in includes:
526             append('sfx2/msg.hxx')
528     #if module == 'sc':
529     #    if 'scslots.hxx' in includes:
530     #        append('sfx2/msg.hxx')
531     return fixes
533 def sort_by_category(list, root, module, filter_local):
534     """ Move all 'system' headers first.
535         Core files of osl, rtl, sal, next.
536         Everything non-module-specific third.
537         Last, module-specific headers.
538     """
539     sys = []
540     boo = []
541     cor = []
542     rst = []
543     mod = []
545     prefix = '<' + module + '/'
546     for i in list:
547         if 'sal/config.h' in i:
548             continue # added unconditionally in fixup
549         if is_c_runtime(i, root, module):
550             sys.append(i)
551         elif '<boost/' in i:
552             boo.append(i)
553         elif prefix in i or not '/' in i:
554             mod.append(i)
555         elif '<sal/' in i or '<vcl/' in i:
556             cor.append(i)
557         elif '<osl/' in i or '<rtl/' in i:
558             if module == "sal": # osl and rtl are also part of sal
559                 mod.append(i)
560             else:
561                 cor.append(i)
562         # Headers from another module that is closely tied to the module.
563         elif module == 'sc' and '<formula' in i:
564             mod.append(i)
565         else:
566             rst.append(i)
568     out = []
569     out += [ "#if PCH_LEVEL >= 1" ]
570     out += sorted(sys)
571     out += sorted(boo)
572     out += [ "#endif // PCH_LEVEL >= 1" ]
573     out += [ "#if PCH_LEVEL >= 2" ]
574     out += sorted(cor)
575     out += [ "#endif // PCH_LEVEL >= 2" ]
576     out += [ "#if PCH_LEVEL >= 3" ]
577     out += sorted(rst)
578     out += [ "#endif // PCH_LEVEL >= 3" ]
579     out += [ "#if PCH_LEVEL >= 4" ]
580     out += sorted(mod)
581     out += [ "#endif // PCH_LEVEL >= 4" ]
582     return out
584 def parse_makefile(groups, lines, lineno, lastif, ifstack):
586     inobjects = False
587     ingeneratedobjects = False
588     inelse = False
589     suffix = 'cxx'
590     os_cond_re = re.compile(r'(ifeq|ifneq)\s*\(\$\(OS\)\,(\w*)\)')
592     line = lines[lineno]
593     if line.startswith('if'):
594         lastif = line
595         if ifstack == 0:
596             # Correction if first line is an if.
597             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
598     else:
599         lineno -= 1
601     while lineno + 1 < len(lines):
602         lineno += 1
603         line = lines[lineno].strip()
604         line = line.rstrip('\\').strip()
605         #print('line #{}: {}'.format(lineno, line))
606         if len(line) == 0:
607             continue
609         if line == '))':
610             inobjects = False
611             ingeneratedobjects = False
612         elif 'add_exception_objects' in line or \
613              'add_cxxobject' in line:
614              inobjects = True
615              #print('inobjects')
616              #if ifstack and not SILENT:
617                 #sys.stderr.write('Sources in a conditional, ignoring for now.\n')
618         elif 'add_generated_exception_objects' in line or \
619              'add_generated_cxxobject' in line:
620              ingeneratedobjects = True
621         elif 'set_generated_cxx_suffix' in line:
622             suffix_re = re.compile('.*set_generated_cxx_suffix,[^,]*,([^)]*).*')
623             match = suffix_re.match(line)
624             if match:
625                 suffix = match.group(1)
626         elif line.startswith('if'):
627             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
628             continue
629         elif line.startswith('endif'):
630             if ifstack:
631                 return lineno
632             continue
633         elif line.startswith('else'):
634             inelse = True
635         elif inobjects or ingeneratedobjects:
636             if EXCLUDE_SYSTEM and ifstack:
637                 continue
638             file = line + '.' + suffix
639             if ',' in line or '(' in line or ')' in line or file.startswith('-'):
640                 #print('passing: ' + line)
641                 pass # $if() probably, or something similar
642             else:
643                 osname = ''
644                 if lastif:
645                     if 'filter' in lastif:
646                         # We can't grok filter, yet.
647                         continue
648                     match = os_cond_re.match(lastif)
649                     if not match:
650                         # We only support OS conditionals.
651                         continue
652                     in_out = match.group(1)
653                     osname = match.group(2) if match else ''
654                     if (in_out == 'ifneq' and not inelse) or \
655                        (in_out == 'ifeq' and inelse):
656                         osname = '!' + osname
658                 if osname not in groups:
659                     groups[osname] = []
660                 if ingeneratedobjects:
661                     file = WORKDIR + '/' + file
662                 groups[osname].append(file)
664     return groups
666 def process_makefile(root, module, libname):
667     """ Parse a gmake makefile and extract
668         source filenames from it.
669     """
671     makefile = 'Library_{}.mk'.format(libname)
672     filename = os.path.join(os.path.join(root, module), makefile)
673     if not os.path.isfile(filename):
674         makefile = 'StaticLibrary_{}.mk'.format(libname)
675         filename = os.path.join(os.path.join(root, module), makefile)
676         if not os.path.isfile(filename):
677             sys.stderr.write('Error: Module {} has no makefile at {}.'.format(module, filename))
679     groups = {'':[], 'ANDROID':[], 'iOS':[], 'WNT':[], 'LINUX':[], 'MACOSX':[]}
681     with open(filename, 'r') as f:
682         lines = f.readlines()
683         groups = parse_makefile(groups, lines, lineno=0, lastif=None, ifstack=0)
685     return groups
687 def is_allowed_if(line, module):
688     """ Check whether the given #if condition
689         is allowed for the given module or whether
690         its block should be ignored.
691     """
693     # remove trailing comments
694     line = re.sub(r'(.*) *//.*', r'\1', line)
695     line = line.strip()
697     # Our sources always build with LIBO_INTERNAL_ONLY.
698     if line == "#if defined LIBO_INTERNAL_ONLY" or line == "#ifdef LIBO_INTERNAL_ONLY":
699         return True
700     # We use PCHs only for C++.
701     if line == "#if defined(__cplusplus)" or line == "#if defined __cplusplus":
702         return True
703     # Debug-specific code, it shouldn't hurt including it unconditionally.
704     if line == "#ifdef DBG_UTIL" or line == "#if OSL_DEBUG_LEVEL > 0":
705         return True
706     if module == "external/skia":
707         # We always set these.
708         if line == "#ifdef SK_VULKAN" or line == "#if SK_GANESH":
709             return True
710     return False
712 def process_source(root, module, filename, maxdepth=0):
713     """ Process a source file to extract
714         included headers.
715         For now, skip on compiler directives.
716         maxdepth is used when processing headers
717         which typically have protecting ifndef.
718     """
720     ifdepth = 0
721     lastif = ''
722     raw_includes = []
723     allowed_ifs = []
724     ifsallowed = 0
725     with open(filename, 'r') as f:
726         for line in f:
727             line = line.strip()
728             if line.startswith('#if'):
729                 if is_allowed_if(line, module):
730                     allowed_ifs.append(True)
731                     ifsallowed += 1
732                 else:
733                     allowed_ifs.append(False)
734                     lastif = line
735                 ifdepth += 1
736             elif line.startswith('#endif'):
737                 ifdepth -= 1
738                 if allowed_ifs[ ifdepth ]:
739                     ifsallowed -= 1
740                 else:
741                     lastif = '#if'
742                 del allowed_ifs[ ifdepth ]
743             elif line.startswith('#pragma once'):
744                 # maxdepth == 1 means we are parsing a header file
745                 # and are allowed one #ifdef block (the include guard),
746                 # but in the #pragma once case do not allow that
747                 assert maxdepth == 1
748                 maxdepth = 0
749             elif line.startswith('#include'):
750                 if ifdepth - ifsallowed <= maxdepth:
751                     line = sanitize(line)
752                     if line:
753                         line = get_filename(line)
754                     if line and len(line):
755                         raw_includes.append(line)
756                 elif not SILENT:
757                     sys.stderr.write('#include in {} : {}\n'.format(lastif, line))
759     return raw_includes
761 def explode(root, module, includes, tree, filter_local, recurse):
762     incpath = os.path.join(root, 'include')
764     for inc in includes:
765         filename = get_filename(inc)
766         if filename in tree or len(filter_local.proc(filename)) == 0:
767             continue
769         try:
770             # Module or Local header.
771             filepath = filter_local.find_local_file(inc)
772             if filepath:
773                 #print('trying loc: ' + filepath)
774                 incs = process_source(root, module, filepath, maxdepth=1)
775                 incs = map(get_filename, incs)
776                 incs = process_list(incs, lambda x: filter_ignore(x, module))
777                 incs = process_list(incs, filter_local.proc)
778                 tree[filename] = incs
779                 if recurse:
780                     tree = explode(root, module, incs, tree, filter_local, recurse)
781                 #print('{} => {}'.format(filepath, tree[filename]))
782                 continue
783         except:
784             pass
786         try:
787             # Public header.
788             filepath = os.path.join(incpath, filename)
789             #print('trying pub: ' + filepath)
790             incs = process_source(root, module, filepath, maxdepth=1)
791             incs = map(get_filename, incs)
792             incs = process_list(incs, lambda x: filter_ignore(x, module))
793             incs = process_list(incs, filter_local.proc)
794             tree[filename] = incs
795             if recurse:
796                 tree = explode(root, module, incs, tree, filter_local, recurse)
797             #print('{} => {}'.format(filepath, tree[filename]))
798             continue
799         except:
800             pass
802         # Failed, but remember to avoid searching again.
803         tree[filename] = []
805     return tree
807 def make_command_line():
808     args = sys.argv[:]
809     # Remove command line flags and
810     # use internal flags.
811     for i in range(len(args)-1, 0, -1):
812         if args[i].startswith('--'):
813             args.pop(i)
815     args.append('--cutoff=' + str(CUTOFF))
816     if EXCLUDE_SYSTEM:
817         args.append('--exclude:system')
818     else:
819         args.append('--include:system')
820     if EXCLUDE_MODULE:
821         args.append('--exclude:module')
822     else:
823         args.append('--include:module')
824     if EXCLUDE_LOCAL:
825         args.append('--exclude:local')
826     else:
827         args.append('--include:local')
829     return ' '.join(args)
831 def generate_includes(includes):
832     """Generates the include lines of the pch.
833     """
834     lines = []
835     for osname, group in includes.items():
836         if not len(group):
837             continue
839         if len(osname):
840             not_eq = ''
841             if osname[0] == '!':
842                 not_eq = '!'
843                 osname = osname[1:]
844             lines.append('')
845             lines.append('#if {}defined({})'.format(not_eq, osname))
847         for i in group:
848             lines.append(i)
850         if len(osname):
851             lines.append('#endif')
853     return lines
855 def generate(includes, libname, filename, module):
856     header = \
857 """/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
859  * This file is part of the LibreOffice project.
861  * This Source Code Form is subject to the terms of the Mozilla Public
862  * License, v. 2.0. If a copy of the MPL was not distributed with this
863  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
864  */
867  This file has been autogenerated by update_pch.sh. It is possible to edit it
868  manually (such as when an include file has been moved/renamed/removed). All such
869  manual changes will be rewritten by the next run of update_pch.sh (which presumably
870  also fixes all possible problems, so it's usually better to use it).
873     footer = \
875 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
877     import datetime
879     with open(filename, 'w') as f:
880         f.write(header)
881         f.write('\n Generated on {} using:\n {}\n'.format(
882                 datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
883                 make_command_line()))
884         f.write('\n If after updating build fails, use the following command to locate conflicting headers:\n ./bin/update_pch_bisect {} "make {}.build" --find-conflicts\n*/\n'.format(
885                 filename, module))
887         # sal needs this for rand_s()
888         if module == 'sal' and libname == 'sal':
889             sal_define = """
890 #if defined(_WIN32)
891 #define _CRT_RAND_S
892 #endif
894             f.write(sal_define)
896         # Dump the headers.
897         f.write('\n')
898         for i in includes:
899             f.write(i + '\n')
901         # Some libraries pull windows headers that aren't self contained.
902         if (module == 'connectivity' and libname == 'ado') or \
903            (module == 'xmlsecurity' and libname == 'xsec_xmlsec'):
904             ado_define = """
905 // Cleanup windows header macro pollution.
906 #if defined(_WIN32) && defined(WINAPI)
907 #include <postwin.h>
908 #undef RGB
909 #endif
911             f.write(ado_define)
913         f.write(footer)
915 def remove_from_tree(filename, tree):
916     # Remove this file, if top-level.
917     incs = tree.pop(filename, [])
918     for i in incs:
919         tree = remove_from_tree(i, tree)
921     # Also remove if included from another.
922     for (k, v) in tree.items():
923         if filename in v:
924             v.remove(filename)
926     return tree
928 def tree_to_list(includes, filename, tree):
929     if filename in includes:
930         return includes
931     includes.append(filename)
932     #incs = tree.pop(filename, [])
933     incs = tree[filename] if filename in tree else []
934     for i in incs:
935         tree_to_list(includes, i, tree)
937     return includes
939 def promote(includes):
940     """ Common library headers are heavily
941         referenced, even if they are included
942         from a few places.
943         Here we separate them to promote
944         their inclusion in the final pch.
945     """
946     promo = []
947     for inc in includes:
948         if inc.startswith('boost') or \
949            inc.startswith('sal') or \
950            inc.startswith('osl') or \
951            inc.startswith('rtl'):
952             promo.append(inc)
953     return promo
955 def make_pch_filename(root, module, libname):
956     """ PCH files are stored here:
957         <root>/<module>/inc/pch/precompiled_<libname>.hxx
958     """
960     path = os.path.join(root, module)
961     path = os.path.join(path, 'inc')
962     path = os.path.join(path, 'pch')
963     path = os.path.join(path, 'precompiled_' + libname + '.hxx')
964     return path
966 def main():
968     global CUTOFF
969     global EXCLUDE_MODULE
970     global EXCLUDE_LOCAL
971     global EXCLUDE_SYSTEM
972     global SILENT
973     global WORKDIR
975     if os.getenv('WORKDIR'):
976         WORKDIR = os.getenv('WORKDIR')
978     root = '.'
979     module = sys.argv[1]
980     libname = sys.argv[2]
981     header = make_pch_filename(root, module, libname)
983     if not os.path.exists(os.path.join(root, module)):
984         raise Exception('Error: module [{}] not found.'.format(module))
986     key = '{}.{}'.format(module, libname)
987     if key in DEFAULTS:
988         # Load the module-specific defaults.
989         CUTOFF = DEFAULTS[key][0]
990         EXCLUDE_SYSTEM = DEFAULTS[key][1]
991         EXCLUDE_MODULE = DEFAULTS[key][2]
992         EXCLUDE_LOCAL = DEFAULTS[key][3]
994     force_update = False
995     for x in range(3, len(sys.argv)):
996         i = sys.argv[x]
997         if i.startswith('--cutoff='):
998             CUTOFF = int(i.split('=')[1])
999         elif i.startswith('--exclude:'):
1000             cat = i.split(':')[1]
1001             if cat == 'module':
1002                 EXCLUDE_MODULE = True
1003             elif cat == 'local':
1004                 EXCLUDE_LOCAL = True
1005             elif cat == 'system':
1006                 EXCLUDE_SYSTEM = True
1007         elif i.startswith('--include:'):
1008             cat = i.split(':')[1]
1009             if cat == 'module':
1010                 EXCLUDE_MODULE = False
1011             elif cat == 'local':
1012                 EXCLUDE_LOCAL = False
1013             elif cat == 'system':
1014                 EXCLUDE_SYSTEM = False
1015         elif i == '--silent':
1016             SILENT = True
1017         elif i == '--force':
1018             force_update = True
1019         else:
1020             sys.stderr.write('Unknown option [{}].'.format(i))
1021             return 1
1023     filter_local = Filter_Local(root, module, \
1024                                 not EXCLUDE_MODULE, \
1025                                 not EXCLUDE_LOCAL)
1027     # Read input.
1028     groups = process_makefile(root, module, libname)
1030     generic = []
1031     for osname, group in groups.items():
1032         if not len(group):
1033             continue
1035         includes = []
1036         for filename in group:
1037             includes += process_source(root, module, filename)
1039         # Save unique top-level includes.
1040         unique = set(includes)
1041         promoted = promote(unique)
1043         # Process includes.
1044         includes = remove_rare(includes)
1045         includes = process_list(includes, lambda x: filter_ignore(x, module))
1046         includes = process_list(includes, filter_local.proc)
1048         # Remove the already included ones.
1049         for inc in includes:
1050             unique.discard(inc)
1052         # Explode the excluded ones.
1053         tree = {i:[] for i in includes}
1054         tree = explode(root, module, unique, tree, filter_local, not EXCLUDE_MODULE)
1056         # Remove the already included ones from the tree.
1057         for inc in includes:
1058             filename = get_filename(inc)
1059             tree = remove_from_tree(filename, tree)
1061         extra = []
1062         for (k, v) in tree.items():
1063             extra += tree_to_list([], k, tree)
1065         promoted += promote(extra)
1066         promoted = process_list(promoted, lambda x: filter_ignore(x, module))
1067         promoted = process_list(promoted, filter_local.proc)
1068         promoted = set(promoted)
1069         # If a promoted header includes others, remove the rest.
1070         for (k, v) in tree.items():
1071             if k in promoted:
1072                 for i in v:
1073                     promoted.discard(i)
1074         includes += [x for x in promoted]
1076         extra = remove_rare(extra)
1077         extra = process_list(extra, lambda x: filter_ignore(x, module))
1078         extra = process_list(extra, filter_local.proc)
1079         includes += extra
1081         includes = [x for x in set(includes)]
1082         fixes = fixup(includes, module)
1083         fixes = map(lambda x: '#include <' + x + '>', fixes)
1085         includes = map(lambda x: '#include <' + x + '>', includes)
1086         sorted = sort_by_category(includes, root, module, filter_local)
1087         includes = list(fixes) + sorted
1089         if len(osname):
1090             for i in generic:
1091                 if i in includes:
1092                     includes.remove(i)
1094         groups[osname] = includes
1095         if not len(osname):
1096             generic = includes
1098     # Open the old pch and compare its contents
1099     # with new includes.
1100     # Clobber only if they are different.
1101     with open(header, 'r') as f:
1102         old_pch_lines = [x.strip() for x in f.readlines()]
1103         new_lines = generate_includes(groups)
1104         # Find the first include in the old pch.
1105         start = -1
1106         for i in range(len(old_pch_lines)):
1107             if old_pch_lines[i].startswith('#include') or old_pch_lines[i].startswith('#if PCH_LEVEL'):
1108                 start = i
1109                 break
1110         # Clobber if there is a mismatch.
1111         if force_update or start < 0 or (len(old_pch_lines) - start < len(new_lines)):
1112             generate(new_lines, libname, header, module)
1113             return 0
1114         else:
1115             for i in range(len(new_lines)):
1116                 if new_lines[i] != old_pch_lines[start + i]:
1117                     generate(new_lines, libname, header, module)
1118                     return 0
1119             else:
1120                 # Identical, but see if new pch removed anything.
1121                 for i in range(start + len(new_lines), len(old_pch_lines)):
1122                     if '#include' in old_pch_lines[i]:
1123                         generate(new_lines, libname, header, module)
1124                         return 0
1126     # Didn't update.
1127     # Use exit code 2 to distinguish it from exit code 1 used e.g. when an exception occurs.
1128     return 2
1130 if __name__ == '__main__':
1131     """ Process all the includes in a Module
1132         to make into a PCH file.
1133         Run without arguments for unittests,
1134         and to see usage.
1135     """
1137     if len(sys.argv) >= 3:
1138         status = main()
1139         sys.exit(status)
1141     print('Usage: {} <Module name> <Library name> [options]'.format(sys.argv[0]))
1142     print('    Always run from the root of LO repository.\n')
1143     print('    Options:')
1144     print('    --cutoff=<count> - Threshold to excluding headers.')
1145     print('    --exclude:<category> - Exclude category-specific headers.')
1146     print('    --include:<category> - Include category-specific headers.')
1147     print('    --force - Force updating the pch even when nothing changes.')
1148     print('    Categories:')
1149     print('         module - Headers in /inc directory of a module.')
1150     print('         local  - Headers local to a source file.')
1151     print('         system - Platform-specific headers.')
1152     print('    --silent - print only errors.')
1153     print('\nRunning unit-tests...')
1156 class TestMethods(unittest.TestCase):
1158     def test_sanitize(self):
1159         self.assertEqual(sanitize('#include "blah/file.cxx"'),
1160                                 '#include <blah/file.cxx>')
1161         self.assertEqual(sanitize('  #include\t"blah/file.cxx" '),
1162                                 '#include <blah/file.cxx>')
1163         self.assertEqual(sanitize('  '),
1164                                 '')
1166     def test_filter_ignore(self):
1167         self.assertEqual(filter_ignore('blah/file.cxx', 'mod'),
1168                                      '')
1169         self.assertEqual(filter_ignore('vector', 'mod'),
1170                                      'vector')
1171         self.assertEqual(filter_ignore('file.cxx', 'mod'),
1172                                      '')
1174     def test_remove_rare(self):
1175         self.assertEqual(remove_rare([]),
1176                                 [])
1178 class TestMakefileParser(unittest.TestCase):
1180     def setUp(self):
1181         global EXCLUDE_SYSTEM
1182         EXCLUDE_SYSTEM = False
1184     def test_parse_singleline_eval(self):
1185         source = "$(eval $(call gb_Library_Library,sal))"
1186         lines = source.split('\n')
1187         groups = {'':[]}
1188         groups = parse_makefile(groups, lines, 0, None, 0)
1189         self.assertEqual(len(groups), 1)
1190         self.assertEqual(len(groups['']), 0)
1192     def test_parse_multiline_eval(self):
1193         source = """$(eval $(call gb_Library_set_include,sal,\\
1194     $$(INCLUDE) \\
1195     -I$(SRCDIR)/sal/inc \\
1198         lines = source.split('\n')
1199         groups = {'':[]}
1200         groups = parse_makefile(groups, lines, 0, None, 0)
1201         self.assertEqual(len(groups), 1)
1202         self.assertEqual(len(groups['']), 0)
1204     def test_parse_multiline_eval_with_if(self):
1205         source = """$(eval $(call gb_Library_add_defs,sal,\\
1206     $(if $(filter $(OS),iOS), \\
1207         -DNO_CHILD_PROCESSES \\
1208     ) \\
1211         lines = source.split('\n')
1212         groups = {'':[]}
1213         groups = parse_makefile(groups, lines, 0, None, 0)
1214         self.assertEqual(len(groups), 1)
1215         self.assertEqual(len(groups['']), 0)
1217     def test_parse_multiline_add_with_if(self):
1218         source = """$(eval $(call gb_Library_add_exception_objects,sal,\\
1219     sal/osl/unx/time \\
1220         $(if $(filter DESKTOP,$(BUILD_TYPE)), sal/osl/unx/salinit) \\
1223         lines = source.split('\n')
1224         groups = {'':[]}
1225         groups = parse_makefile(groups, lines, 0, None, 0)
1226         self.assertEqual(len(groups), 1)
1227         self.assertEqual(len(groups['']), 1)
1228         self.assertEqual(groups[''][0], 'sal/osl/unx/time.cxx')
1230     def test_parse_if_else(self):
1231         source = """ifeq ($(OS),MACOSX)
1232 $(eval $(call gb_Library_add_exception_objects,sal,\\
1233     sal/osl/mac/mac \\
1235 else
1236 $(eval $(call gb_Library_add_exception_objects,sal,\\
1237     sal/osl/unx/uunxapi \\
1239 endif
1241         lines = source.split('\n')
1242         groups = {'':[]}
1243         groups = parse_makefile(groups, lines, 0, None, 0)
1244         self.assertEqual(len(groups), 3)
1245         self.assertEqual(len(groups['']), 0)
1246         self.assertEqual(len(groups['MACOSX']), 1)
1247         self.assertEqual(len(groups['!MACOSX']), 1)
1248         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1249         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1251     def test_parse_nested_if(self):
1252         source = """ifeq ($(OS),MACOSX)
1253 $(eval $(call gb_Library_add_exception_objects,sal,\\
1254     sal/osl/mac/mac \\
1256 else
1257 $(eval $(call gb_Library_add_exception_objects,sal,\\
1258     sal/osl/unx/uunxapi \\
1261 ifeq ($(OS),LINUX)
1262 $(eval $(call gb_Library_add_exception_objects,sal,\\
1263     sal/textenc/context \\
1265 endif
1266 endif
1268         lines = source.split('\n')
1269         groups = {'':[]}
1270         groups = parse_makefile(groups, lines, 0, None, 0)
1271         self.assertEqual(len(groups), 4)
1272         self.assertEqual(len(groups['']), 0)
1273         self.assertEqual(len(groups['MACOSX']), 1)
1274         self.assertEqual(len(groups['!MACOSX']), 1)
1275         self.assertEqual(len(groups['LINUX']), 1)
1276         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1277         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1278         self.assertEqual(groups['LINUX'][0], 'sal/textenc/context.cxx')
1280     def test_parse_exclude_system(self):
1281         source = """ifeq ($(OS),MACOSX)
1282 $(eval $(call gb_Library_add_exception_objects,sal,\\
1283     sal/osl/mac/mac \\
1285 else
1286 $(eval $(call gb_Library_add_exception_objects,sal,\\
1287     sal/osl/unx/uunxapi \\
1290 ifeq ($(OS),LINUX)
1291 $(eval $(call gb_Library_add_exception_objects,sal,\\
1292     sal/textenc/context \\
1294 endif
1295 endif
1297         global EXCLUDE_SYSTEM
1298         EXCLUDE_SYSTEM = True
1300         lines = source.split('\n')
1301         groups = {'':[]}
1302         groups = parse_makefile(groups, lines, 0, None, 0)
1303         self.assertEqual(len(groups), 1)
1304         self.assertEqual(len(groups['']), 0)
1306     def test_parse_filter(self):
1307         source = """ifneq ($(filter $(OS),MACOSX iOS),)
1308 $(eval $(call gb_Library_add_exception_objects,sal,\\
1309     sal/osl/unx/osxlocale \\
1311 endif
1313         # Filter is still unsupported.
1314         lines = source.split('\n')
1315         groups = {'':[]}
1316         groups = parse_makefile(groups, lines, 0, None, 0)
1317         self.assertEqual(len(groups), 1)
1318         self.assertEqual(len(groups['']), 0)
1320 unittest.main()
1322 # vim: set et sw=4 ts=4 expandtab: