Fix typo
[LibreOffice.git] / bin / update_pch
blob4719e5f9269fc68d0e926b629c0b8ca588aefb7f
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     'tools.tl'                          : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #   4.2
113     'unotools.utl'                      : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
114     'unoxml.unoxml'                     : ( 1, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
115     'uui.uui'                           : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   4.9
116     'vbahelper.msforms'                 : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   5.2
117     'vbahelper.vbahelper'               : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
118     'vcl.vcl'                           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  35.7
119     'writerfilter.writerfilter'         : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #  19.7/27.3
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             'xmlsecurity/xmlsec-wrapper.h',
442             ]
443     if module == 'external/pdfium':
444         ignore_list += [
445             'third_party/freetype/include/pstables.h',
446             ]
447     if module == 'external/clucene':
448         ignore_list += [
449             '_bufferedstream.h',
450             '_condition.h',
451             '_gunichartables.h',
452             '_threads.h',
453             'error.h',
454             'CLucene/LuceneThreads.h',
455             'CLucene/config/_threads.h',
456             ]
457     if module == 'external/skia':
458         ignore_list += [
459             'skcms_internal.h',
460             'zlib.h', # causes crc32 conflict
461             'dirent.h', # unix-specific
462             'pthread.h',
463             'unistd.h',
464             'sys/stat.h',
465             'ft2build.h',
466             'fontconfig/fontconfig.h',
467             'GL/glx.h',
468             'src/Transform_inl.h',
469             'src/c/sk_c_from_to.h',
470             'src/c/sk_types_priv.h',
471             'src/core/SkBlitBWMaskTemplate.h',
472             'src/sfnt/SkSFNTHeader.h',
473             'src/opts/',
474             'src/core/SkCubicSolver.h',
475             'src/sksl/SkSLCPP.h',
476             'src/gpu/vk/GrVkAMDMemoryAllocator.h',
477             'src/gpu/GrUtil.h',
478             'src/sksl/', # conflict between SkSL::Expression and SkSL::dsl::Expression
479             'include/sksl/',
480             'src/gpu/vk/',
481             'include/gpu/vk'
482             ]
483     if module == 'external/zxing':
484         ignore_list += [
485             'rss/ODRSSExpandedBinaryDecoder.h'
486             ]
488     for i in ignore_list:
489         if line.startswith(i):
490             return ''
491         if i[0] == '*' and line.endswith(i[1:]):
492             return ''
493         if i[-1] == '*' and line.startswith(i[:-1]):
494             return ''
496     return line
498 def fixup(includes, module):
499     """ Here we add any headers
500         necessary in the pch.
501         These could be known to be very
502         common but for technical reasons
503         left out of the pch by this generator.
504         Or, they could be missing from the
505         source files where they are used
506         (probably because they had been
507         in the old pch, they were missed).
508         Also, these could be headers
509         that make the build faster but
510         aren't added automatically.
511     """
512     fixes = []
513     def append(inc):
514         # Add a space to exclude from
515         # ignore bisecting.
516         line = ' #include <{}>'.format(inc)
517         try:
518             i = fixes.index(inc)
519             fixes[i] = inc
520         except:
521             fixes.append(inc)
523     append('sal/config.h')
525     if module == 'basctl':
526         if 'basslots.hxx' in includes:
527             append('sfx2/msg.hxx')
529     #if module == 'sc':
530     #    if 'scslots.hxx' in includes:
531     #        append('sfx2/msg.hxx')
532     return fixes
534 def sort_by_category(list, root, module, filter_local):
535     """ Move all 'system' headers first.
536         Core files of osl, rtl, sal, next.
537         Everything non-module-specific third.
538         Last, module-specific headers.
539     """
540     sys = []
541     boo = []
542     cor = []
543     rst = []
544     mod = []
546     prefix = '<' + module + '/'
547     for i in list:
548         if 'sal/config.h' in i:
549             continue # added unconditionally in fixup
550         if is_c_runtime(i, root, module):
551             sys.append(i)
552         elif '<boost/' in i:
553             boo.append(i)
554         elif prefix in i or not '/' in i:
555             mod.append(i)
556         elif '<sal/' in i or '<vcl/' in i:
557             cor.append(i)
558         elif '<osl/' in i or '<rtl/' in i:
559             if module == "sal": # osl and rtl are also part of sal
560                 mod.append(i)
561             else:
562                 cor.append(i)
563         # Headers from another module that is closely tied to the module.
564         elif module == 'sc' and '<formula' in i:
565             mod.append(i)
566         else:
567             rst.append(i)
569     out = []
570     out += [ "#if PCH_LEVEL >= 1" ]
571     out += sorted(sys)
572     out += sorted(boo)
573     out += [ "#endif // PCH_LEVEL >= 1" ]
574     out += [ "#if PCH_LEVEL >= 2" ]
575     out += sorted(cor)
576     out += [ "#endif // PCH_LEVEL >= 2" ]
577     out += [ "#if PCH_LEVEL >= 3" ]
578     out += sorted(rst)
579     out += [ "#endif // PCH_LEVEL >= 3" ]
580     out += [ "#if PCH_LEVEL >= 4" ]
581     out += sorted(mod)
582     out += [ "#endif // PCH_LEVEL >= 4" ]
583     return out
585 def parse_makefile(groups, lines, lineno, lastif, ifstack):
587     inobjects = False
588     ingeneratedobjects = False
589     inelse = False
590     suffix = 'cxx'
591     os_cond_re = re.compile(r'(ifeq|ifneq)\s*\(\$\(OS\)\,(\w*)\)')
593     line = lines[lineno]
594     if line.startswith('if'):
595         lastif = line
596         if ifstack == 0:
597             # Correction if first line is an if.
598             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
599     else:
600         lineno -= 1
602     while lineno + 1 < len(lines):
603         lineno += 1
604         line = lines[lineno].strip()
605         line = line.rstrip('\\').strip()
606         #print('line #{}: {}'.format(lineno, line))
607         if len(line) == 0:
608             continue
610         if line == '))':
611             inobjects = False
612             ingeneratedobjects = False
613         elif 'add_exception_objects' in line or \
614              'add_cxxobject' in line:
615              inobjects = True
616              #print('inobjects')
617              #if ifstack and not SILENT:
618                 #sys.stderr.write('Sources in a conditional, ignoring for now.\n')
619         elif 'add_generated_exception_objects' in line or \
620              'add_generated_cxxobject' in line:
621              ingeneratedobjects = True
622         elif 'set_generated_cxx_suffix' in line:
623             suffix_re = re.compile('.*set_generated_cxx_suffix,[^,]*,([^)]*).*')
624             match = suffix_re.match(line)
625             if match:
626                 suffix = match.group(1)
627         elif line.startswith('if'):
628             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
629             continue
630         elif line.startswith('endif'):
631             if ifstack:
632                 return lineno
633             continue
634         elif line.startswith('else'):
635             inelse = True
636         elif inobjects or ingeneratedobjects:
637             if EXCLUDE_SYSTEM and ifstack:
638                 continue
639             file = line + '.' + suffix
640             if ',' in line or '(' in line or ')' in line or file.startswith('-'):
641                 #print('passing: ' + line)
642                 pass # $if() probably, or something similar
643             else:
644                 osname = ''
645                 if lastif:
646                     if 'filter' in lastif:
647                         # We can't grok filter, yet.
648                         continue
649                     match = os_cond_re.match(lastif)
650                     if not match:
651                         # We only support OS conditionals.
652                         continue
653                     in_out = match.group(1)
654                     osname = match.group(2) if match else ''
655                     if (in_out == 'ifneq' and not inelse) or \
656                        (in_out == 'ifeq' and inelse):
657                         osname = '!' + osname
659                 if osname not in groups:
660                     groups[osname] = []
661                 if ingeneratedobjects:
662                     file = WORKDIR + '/' + file
663                 groups[osname].append(file)
665     return groups
667 def process_makefile(root, module, libname):
668     """ Parse a gmake makefile and extract
669         source filenames from it.
670     """
672     makefile = 'Library_{}.mk'.format(libname)
673     filename = os.path.join(os.path.join(root, module), makefile)
674     if not os.path.isfile(filename):
675         makefile = 'StaticLibrary_{}.mk'.format(libname)
676         filename = os.path.join(os.path.join(root, module), makefile)
677         if not os.path.isfile(filename):
678             sys.stderr.write('Error: Module {} has no makefile at {}.'.format(module, filename))
680     groups = {'':[], 'ANDROID':[], 'iOS':[], 'WNT':[], 'LINUX':[], 'MACOSX':[]}
682     with open(filename, 'r') as f:
683         lines = f.readlines()
684         groups = parse_makefile(groups, lines, lineno=0, lastif=None, ifstack=0)
686     return groups
688 def is_allowed_if(line, module):
689     """ Check whether the given #if condition
690         is allowed for the given module or whether
691         its block should be ignored.
692     """
694     # remove trailing comments
695     line = re.sub(r'(.*) *//.*', r'\1', line)
696     line = line.strip()
698     # Our sources always build with LIBO_INTERNAL_ONLY.
699     if line == "#if defined LIBO_INTERNAL_ONLY" or line == "#ifdef LIBO_INTERNAL_ONLY":
700         return True
701     # We use PCHs only for C++.
702     if line == "#if defined(__cplusplus)" or line == "#if defined __cplusplus":
703         return True
704     # Debug-specific code, it shouldn't hurt including it unconditionally.
705     if line == "#ifdef DBG_UTIL" or line == "#if OSL_DEBUG_LEVEL > 0":
706         return True
707     if module == "external/skia":
708         # We always set these.
709         if line == "#ifdef SK_VULKAN" or line == "#if SK_GANESH":
710             return True
711     return False
713 def process_source(root, module, filename, maxdepth=0):
714     """ Process a source file to extract
715         included headers.
716         For now, skip on compiler directives.
717         maxdepth is used when processing headers
718         which typically have protecting ifndef.
719     """
721     ifdepth = 0
722     lastif = ''
723     raw_includes = []
724     allowed_ifs = []
725     ifsallowed = 0
726     with open(filename, 'r') as f:
727         for line in f:
728             line = line.strip()
729             if line.startswith('#if'):
730                 if is_allowed_if(line, module):
731                     allowed_ifs.append(True)
732                     ifsallowed += 1
733                 else:
734                     allowed_ifs.append(False)
735                     lastif = line
736                 ifdepth += 1
737             elif line.startswith('#endif'):
738                 ifdepth -= 1
739                 if allowed_ifs[ ifdepth ]:
740                     ifsallowed -= 1
741                 else:
742                     lastif = '#if'
743                 del allowed_ifs[ ifdepth ]
744             elif line.startswith('#pragma once'):
745                 # maxdepth == 1 means we are parsing a header file
746                 # and are allowed one #ifdef block (the include guard),
747                 # but in the #pragma once case do not allow that
748                 assert maxdepth == 1
749                 maxdepth = 0
750             elif line.startswith('#include'):
751                 if ifdepth - ifsallowed <= maxdepth:
752                     line = sanitize(line)
753                     if line:
754                         line = get_filename(line)
755                     if line and len(line):
756                         raw_includes.append(line)
757                 elif not SILENT:
758                     sys.stderr.write('#include in {} : {}\n'.format(lastif, line))
760     return raw_includes
762 def explode(root, module, includes, tree, filter_local, recurse):
763     incpath = os.path.join(root, 'include')
765     for inc in includes:
766         filename = get_filename(inc)
767         if filename in tree or len(filter_local.proc(filename)) == 0:
768             continue
770         try:
771             # Module or Local header.
772             filepath = filter_local.find_local_file(inc)
773             if filepath:
774                 #print('trying loc: ' + filepath)
775                 incs = process_source(root, module, filepath, maxdepth=1)
776                 incs = map(get_filename, incs)
777                 incs = process_list(incs, lambda x: filter_ignore(x, module))
778                 incs = process_list(incs, filter_local.proc)
779                 tree[filename] = incs
780                 if recurse:
781                     tree = explode(root, module, incs, tree, filter_local, recurse)
782                 #print('{} => {}'.format(filepath, tree[filename]))
783                 continue
784         except:
785             pass
787         try:
788             # Public header.
789             filepath = os.path.join(incpath, filename)
790             #print('trying pub: ' + filepath)
791             incs = process_source(root, module, filepath, maxdepth=1)
792             incs = map(get_filename, incs)
793             incs = process_list(incs, lambda x: filter_ignore(x, module))
794             incs = process_list(incs, filter_local.proc)
795             tree[filename] = incs
796             if recurse:
797                 tree = explode(root, module, incs, tree, filter_local, recurse)
798             #print('{} => {}'.format(filepath, tree[filename]))
799             continue
800         except:
801             pass
803         # Failed, but remember to avoid searching again.
804         tree[filename] = []
806     return tree
808 def make_command_line():
809     args = sys.argv[:]
810     # Remove command line flags and
811     # use internal flags.
812     for i in range(len(args)-1, 0, -1):
813         if args[i].startswith('--'):
814             args.pop(i)
816     args.append('--cutoff=' + str(CUTOFF))
817     if EXCLUDE_SYSTEM:
818         args.append('--exclude:system')
819     else:
820         args.append('--include:system')
821     if EXCLUDE_MODULE:
822         args.append('--exclude:module')
823     else:
824         args.append('--include:module')
825     if EXCLUDE_LOCAL:
826         args.append('--exclude:local')
827     else:
828         args.append('--include:local')
830     return ' '.join(args)
832 def generate_includes(includes):
833     """Generates the include lines of the pch.
834     """
835     lines = []
836     for osname, group in includes.items():
837         if not len(group):
838             continue
840         if len(osname):
841             not_eq = ''
842             if osname[0] == '!':
843                 not_eq = '!'
844                 osname = osname[1:]
845             lines.append('')
846             lines.append('#if {}defined({})'.format(not_eq, osname))
848         for i in group:
849             lines.append(i)
851         if len(osname):
852             lines.append('#endif')
854     return lines
856 def generate(includes, libname, filename, module):
857     header = \
858 """/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
860  * This file is part of the LibreOffice project.
862  * This Source Code Form is subject to the terms of the Mozilla Public
863  * License, v. 2.0. If a copy of the MPL was not distributed with this
864  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
865  */
868  This file has been autogenerated by update_pch.sh. It is possible to edit it
869  manually (such as when an include file has been moved/renamed/removed). All such
870  manual changes will be rewritten by the next run of update_pch.sh (which presumably
871  also fixes all possible problems, so it's usually better to use it).
874     footer = \
876 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
878     import datetime
880     with open(filename, 'w') as f:
881         f.write(header)
882         f.write('\n Generated on {} using:\n {}\n'.format(
883                 datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
884                 make_command_line()))
885         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(
886                 filename, module))
888         # sal needs this for rand_s()
889         if module == 'sal' and libname == 'sal':
890             sal_define = """
891 #if defined(_WIN32)
892 #define _CRT_RAND_S
893 #endif
895             f.write(sal_define)
897         # Dump the headers.
898         f.write('\n')
899         for i in includes:
900             f.write(i + '\n')
902         # Some libraries pull windows headers that aren't self contained.
903         if (module == 'connectivity' and libname == 'ado') or \
904            (module == 'xmlsecurity' and libname == 'xsec_xmlsec'):
905             ado_define = """
906 // Cleanup windows header macro pollution.
907 #if defined(_WIN32) && defined(WINAPI)
908 #include <postwin.h>
909 #undef RGB
910 #endif
912             f.write(ado_define)
914         f.write(footer)
916 def remove_from_tree(filename, tree):
917     # Remove this file, if top-level.
918     incs = tree.pop(filename, [])
919     for i in incs:
920         tree = remove_from_tree(i, tree)
922     # Also remove if included from another.
923     for (k, v) in tree.items():
924         if filename in v:
925             v.remove(filename)
927     return tree
929 def tree_to_list(includes, filename, tree):
930     if filename in includes:
931         return includes
932     includes.append(filename)
933     #incs = tree.pop(filename, [])
934     incs = tree[filename] if filename in tree else []
935     for i in incs:
936         tree_to_list(includes, i, tree)
938     return includes
940 def promote(includes):
941     """ Common library headers are heavily
942         referenced, even if they are included
943         from a few places.
944         Here we separate them to promote
945         their inclusion in the final pch.
946     """
947     promo = []
948     for inc in includes:
949         if inc.startswith('boost') or \
950            inc.startswith('sal') or \
951            inc.startswith('osl') or \
952            inc.startswith('rtl'):
953             promo.append(inc)
954     return promo
956 def make_pch_filename(root, module, libname):
957     """ PCH files are stored here:
958         <root>/<module>/inc/pch/precompiled_<libname>.hxx
959     """
961     path = os.path.join(root, module)
962     path = os.path.join(path, 'inc')
963     path = os.path.join(path, 'pch')
964     path = os.path.join(path, 'precompiled_' + libname + '.hxx')
965     return path
967 def main():
969     global CUTOFF
970     global EXCLUDE_MODULE
971     global EXCLUDE_LOCAL
972     global EXCLUDE_SYSTEM
973     global SILENT
974     global WORKDIR
976     if os.getenv('WORKDIR'):
977         WORKDIR = os.getenv('WORKDIR')
979     root = '.'
980     module = sys.argv[1]
981     libname = sys.argv[2]
982     header = make_pch_filename(root, module, libname)
984     if not os.path.exists(os.path.join(root, module)):
985         raise Exception('Error: module [{}] not found.'.format(module))
987     key = '{}.{}'.format(module, libname)
988     if key in DEFAULTS:
989         # Load the module-specific defaults.
990         CUTOFF = DEFAULTS[key][0]
991         EXCLUDE_SYSTEM = DEFAULTS[key][1]
992         EXCLUDE_MODULE = DEFAULTS[key][2]
993         EXCLUDE_LOCAL = DEFAULTS[key][3]
995     force_update = False
996     for x in range(3, len(sys.argv)):
997         i = sys.argv[x]
998         if i.startswith('--cutoff='):
999             CUTOFF = int(i.split('=')[1])
1000         elif i.startswith('--exclude:'):
1001             cat = i.split(':')[1]
1002             if cat == 'module':
1003                 EXCLUDE_MODULE = True
1004             elif cat == 'local':
1005                 EXCLUDE_LOCAL = True
1006             elif cat == 'system':
1007                 EXCLUDE_SYSTEM = True
1008         elif i.startswith('--include:'):
1009             cat = i.split(':')[1]
1010             if cat == 'module':
1011                 EXCLUDE_MODULE = False
1012             elif cat == 'local':
1013                 EXCLUDE_LOCAL = False
1014             elif cat == 'system':
1015                 EXCLUDE_SYSTEM = False
1016         elif i == '--silent':
1017             SILENT = True
1018         elif i == '--force':
1019             force_update = True
1020         else:
1021             sys.stderr.write('Unknown option [{}].'.format(i))
1022             return 1
1024     filter_local = Filter_Local(root, module, \
1025                                 not EXCLUDE_MODULE, \
1026                                 not EXCLUDE_LOCAL)
1028     # Read input.
1029     groups = process_makefile(root, module, libname)
1031     generic = []
1032     for osname, group in groups.items():
1033         if not len(group):
1034             continue
1036         includes = []
1037         for filename in group:
1038             includes += process_source(root, module, filename)
1040         # Save unique top-level includes.
1041         unique = set(includes)
1042         promoted = promote(unique)
1044         # Process includes.
1045         includes = remove_rare(includes)
1046         includes = process_list(includes, lambda x: filter_ignore(x, module))
1047         includes = process_list(includes, filter_local.proc)
1049         # Remove the already included ones.
1050         for inc in includes:
1051             unique.discard(inc)
1053         # Explode the excluded ones.
1054         tree = {i:[] for i in includes}
1055         tree = explode(root, module, unique, tree, filter_local, not EXCLUDE_MODULE)
1057         # Remove the already included ones from the tree.
1058         for inc in includes:
1059             filename = get_filename(inc)
1060             tree = remove_from_tree(filename, tree)
1062         extra = []
1063         for (k, v) in tree.items():
1064             extra += tree_to_list([], k, tree)
1066         promoted += promote(extra)
1067         promoted = process_list(promoted, lambda x: filter_ignore(x, module))
1068         promoted = process_list(promoted, filter_local.proc)
1069         promoted = set(promoted)
1070         # If a promoted header includes others, remove the rest.
1071         for (k, v) in tree.items():
1072             if k in promoted:
1073                 for i in v:
1074                     promoted.discard(i)
1075         includes += [x for x in promoted]
1077         extra = remove_rare(extra)
1078         extra = process_list(extra, lambda x: filter_ignore(x, module))
1079         extra = process_list(extra, filter_local.proc)
1080         includes += extra
1082         includes = [x for x in set(includes)]
1083         fixes = fixup(includes, module)
1084         fixes = map(lambda x: '#include <' + x + '>', fixes)
1086         includes = map(lambda x: '#include <' + x + '>', includes)
1087         sorted = sort_by_category(includes, root, module, filter_local)
1088         includes = list(fixes) + sorted
1090         if len(osname):
1091             for i in generic:
1092                 if i in includes:
1093                     includes.remove(i)
1095         groups[osname] = includes
1096         if not len(osname):
1097             generic = includes
1099     # Open the old pch and compare its contents
1100     # with new includes.
1101     # Clobber only if they are different.
1102     with open(header, 'r') as f:
1103         old_pch_lines = [x.strip() for x in f.readlines()]
1104         new_lines = generate_includes(groups)
1105         # Find the first include in the old pch.
1106         start = -1
1107         for i in range(len(old_pch_lines)):
1108             if old_pch_lines[i].startswith('#include') or old_pch_lines[i].startswith('#if PCH_LEVEL'):
1109                 start = i
1110                 break
1111         # Clobber if there is a mismatch.
1112         if force_update or start < 0 or (len(old_pch_lines) - start < len(new_lines)):
1113             generate(new_lines, libname, header, module)
1114             return 0
1115         else:
1116             for i in range(len(new_lines)):
1117                 if new_lines[i] != old_pch_lines[start + i]:
1118                     generate(new_lines, libname, header, module)
1119                     return 0
1120             else:
1121                 # Identical, but see if new pch removed anything.
1122                 for i in range(start + len(new_lines), len(old_pch_lines)):
1123                     if '#include' in old_pch_lines[i]:
1124                         generate(new_lines, libname, header, module)
1125                         return 0
1127     # Didn't update.
1128     # Use exit code 2 to distinguish it from exit code 1 used e.g. when an exception occurs.
1129     return 2
1131 if __name__ == '__main__':
1132     """ Process all the includes in a Module
1133         to make into a PCH file.
1134         Run without arguments for unittests,
1135         and to see usage.
1136     """
1138     if len(sys.argv) >= 3:
1139         status = main()
1140         sys.exit(status)
1142     print('Usage: {} <Module name> <Library name> [options]'.format(sys.argv[0]))
1143     print('    Always run from the root of LO repository.\n')
1144     print('    Options:')
1145     print('    --cutoff=<count> - Threshold to excluding headers.')
1146     print('    --exclude:<category> - Exclude category-specific headers.')
1147     print('    --include:<category> - Include category-specific headers.')
1148     print('    --force - Force updating the pch even when nothing changes.')
1149     print('    Categories:')
1150     print('         module - Headers in /inc directory of a module.')
1151     print('         local  - Headers local to a source file.')
1152     print('         system - Platform-specific headers.')
1153     print('    --silent - print only errors.')
1154     print('\nRunning unit-tests...')
1157 class TestMethods(unittest.TestCase):
1159     def test_sanitize(self):
1160         self.assertEqual(sanitize('#include "blah/file.cxx"'),
1161                                 '#include <blah/file.cxx>')
1162         self.assertEqual(sanitize('  #include\t"blah/file.cxx" '),
1163                                 '#include <blah/file.cxx>')
1164         self.assertEqual(sanitize('  '),
1165                                 '')
1167     def test_filter_ignore(self):
1168         self.assertEqual(filter_ignore('blah/file.cxx', 'mod'),
1169                                      '')
1170         self.assertEqual(filter_ignore('vector', 'mod'),
1171                                      'vector')
1172         self.assertEqual(filter_ignore('file.cxx', 'mod'),
1173                                      '')
1175     def test_remove_rare(self):
1176         self.assertEqual(remove_rare([]),
1177                                 [])
1179 class TestMakefileParser(unittest.TestCase):
1181     def setUp(self):
1182         global EXCLUDE_SYSTEM
1183         EXCLUDE_SYSTEM = False
1185     def test_parse_singleline_eval(self):
1186         source = "$(eval $(call gb_Library_Library,sal))"
1187         lines = source.split('\n')
1188         groups = {'':[]}
1189         groups = parse_makefile(groups, lines, 0, None, 0)
1190         self.assertEqual(len(groups), 1)
1191         self.assertEqual(len(groups['']), 0)
1193     def test_parse_multiline_eval(self):
1194         source = """$(eval $(call gb_Library_set_include,sal,\\
1195     $$(INCLUDE) \\
1196     -I$(SRCDIR)/sal/inc \\
1199         lines = source.split('\n')
1200         groups = {'':[]}
1201         groups = parse_makefile(groups, lines, 0, None, 0)
1202         self.assertEqual(len(groups), 1)
1203         self.assertEqual(len(groups['']), 0)
1205     def test_parse_multiline_eval_with_if(self):
1206         source = """$(eval $(call gb_Library_add_defs,sal,\\
1207     $(if $(filter $(OS),iOS), \\
1208         -DNO_CHILD_PROCESSES \\
1209     ) \\
1212         lines = source.split('\n')
1213         groups = {'':[]}
1214         groups = parse_makefile(groups, lines, 0, None, 0)
1215         self.assertEqual(len(groups), 1)
1216         self.assertEqual(len(groups['']), 0)
1218     def test_parse_multiline_add_with_if(self):
1219         source = """$(eval $(call gb_Library_add_exception_objects,sal,\\
1220     sal/osl/unx/time \\
1221         $(if $(filter DESKTOP,$(BUILD_TYPE)), sal/osl/unx/salinit) \\
1224         lines = source.split('\n')
1225         groups = {'':[]}
1226         groups = parse_makefile(groups, lines, 0, None, 0)
1227         self.assertEqual(len(groups), 1)
1228         self.assertEqual(len(groups['']), 1)
1229         self.assertEqual(groups[''][0], 'sal/osl/unx/time.cxx')
1231     def test_parse_if_else(self):
1232         source = """ifeq ($(OS),MACOSX)
1233 $(eval $(call gb_Library_add_exception_objects,sal,\\
1234     sal/osl/mac/mac \\
1236 else
1237 $(eval $(call gb_Library_add_exception_objects,sal,\\
1238     sal/osl/unx/uunxapi \\
1240 endif
1242         lines = source.split('\n')
1243         groups = {'':[]}
1244         groups = parse_makefile(groups, lines, 0, None, 0)
1245         self.assertEqual(len(groups), 3)
1246         self.assertEqual(len(groups['']), 0)
1247         self.assertEqual(len(groups['MACOSX']), 1)
1248         self.assertEqual(len(groups['!MACOSX']), 1)
1249         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1250         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1252     def test_parse_nested_if(self):
1253         source = """ifeq ($(OS),MACOSX)
1254 $(eval $(call gb_Library_add_exception_objects,sal,\\
1255     sal/osl/mac/mac \\
1257 else
1258 $(eval $(call gb_Library_add_exception_objects,sal,\\
1259     sal/osl/unx/uunxapi \\
1262 ifeq ($(OS),LINUX)
1263 $(eval $(call gb_Library_add_exception_objects,sal,\\
1264     sal/textenc/context \\
1266 endif
1267 endif
1269         lines = source.split('\n')
1270         groups = {'':[]}
1271         groups = parse_makefile(groups, lines, 0, None, 0)
1272         self.assertEqual(len(groups), 4)
1273         self.assertEqual(len(groups['']), 0)
1274         self.assertEqual(len(groups['MACOSX']), 1)
1275         self.assertEqual(len(groups['!MACOSX']), 1)
1276         self.assertEqual(len(groups['LINUX']), 1)
1277         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1278         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1279         self.assertEqual(groups['LINUX'][0], 'sal/textenc/context.cxx')
1281     def test_parse_exclude_system(self):
1282         source = """ifeq ($(OS),MACOSX)
1283 $(eval $(call gb_Library_add_exception_objects,sal,\\
1284     sal/osl/mac/mac \\
1286 else
1287 $(eval $(call gb_Library_add_exception_objects,sal,\\
1288     sal/osl/unx/uunxapi \\
1291 ifeq ($(OS),LINUX)
1292 $(eval $(call gb_Library_add_exception_objects,sal,\\
1293     sal/textenc/context \\
1295 endif
1296 endif
1298         global EXCLUDE_SYSTEM
1299         EXCLUDE_SYSTEM = True
1301         lines = source.split('\n')
1302         groups = {'':[]}
1303         groups = parse_makefile(groups, lines, 0, None, 0)
1304         self.assertEqual(len(groups), 1)
1305         self.assertEqual(len(groups['']), 0)
1307     def test_parse_filter(self):
1308         source = """ifneq ($(filter $(OS),MACOSX iOS),)
1309 $(eval $(call gb_Library_add_exception_objects,sal,\\
1310     sal/osl/unx/osxlocale \\
1312 endif
1314         # Filter is still unsupported.
1315         lines = source.split('\n')
1316         groups = {'':[]}
1317         groups = parse_makefile(groups, lines, 0, None, 0)
1318         self.assertEqual(len(groups), 1)
1319         self.assertEqual(len(groups['']), 0)
1321 unittest.main()
1323 # vim: set et sw=4 ts=4 expandtab: