tdf#149529 crash on deref deleted ScDocument*
[LibreOffice.git] / bin / update_pch
blobeb9631e80d80dc210e1faee37fa3f9968e384039
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     'drawinglayer.drawinglayer'         : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   7.4
77     'editeng.editeng'                   : ( 5, EXCLUDE, INCLUDE, EXCLUDE), #  13.0
78     'forms.frm'                         : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #  14.2
79     'framework.fwk'                     : ( 7, EXCLUDE, INCLUDE, INCLUDE), #  14.8
80     'hwpfilter.hwp'                     : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   6.0
81     'lotuswordpro.lwpft'                : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #  11.6
82     'oox.oox'                           : ( 6, EXCLUDE, EXCLUDE, INCLUDE), #  28.2
83     'package.package2'                  : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   4.5
84     'package.xstor'                     : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   3.8
85     'reportdesign.rpt'                  : ( 9, EXCLUDE, INCLUDE, INCLUDE), #   9.4
86     'reportdesign.rptui'                : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  13.1
87     'reportdesign.rptxml'               : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   7.6
88     'sal.sal'                           : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   4.2
89     'sc.sc'                             : (12, EXCLUDE, INCLUDE, INCLUDE), #  92.6
90     'sc.scfilt'                         : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #  39.9
91     'sc.scui'                           : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #  15.0
92     'sc.vbaobj'                         : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #  17.3
93     'sd.sd'                             : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #  47.4
94     'sd.sdui'                           : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   9.4
95     'sdext.PresentationMinimizer'       : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.1
96     'sdext.PresenterScreen'             : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   7.1
97     'sfx2.sfx'                          : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #  27.4
98     'slideshow.slideshow'               : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #  10.8
99     'sot.sot'                           : ( 5, EXCLUDE, EXCLUDE, INCLUDE), #   3.1
100     'starmath.sm'                       : ( 5, EXCLUDE, EXCLUDE, INCLUDE), #  10.9
101     'svgio.svgio'                       : ( 8, EXCLUDE, EXCLUDE, INCLUDE), #   4.3
102     'emfio.emfio'                       : ( 8, EXCLUDE, EXCLUDE, INCLUDE), #   4.3
103     'svl.svl'                           : ( 6, EXCLUDE, EXCLUDE, EXCLUDE), #   7.6
104     'svtools.svt'                       : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #  17.6
105     'svx.svx'                           : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #  20.7
106     'svx.svxcore'                       : ( 7, EXCLUDE, INCLUDE, EXCLUDE), #  37.0
107     'sw.msword'                         : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  22.4
108     'sw.sw'                             : ( 7, EXCLUDE, EXCLUDE, INCLUDE), # 129.6
109     'sw.swui'                           : ( 3, EXCLUDE, INCLUDE, INCLUDE), #  26.1
110     'sw.vbaswobj'                       : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  13.1
111     'tools.tl'                          : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #   4.2
112     'unotools.utl'                      : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
113     'unoxml.unoxml'                     : ( 1, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
114     'uui.uui'                           : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   4.9
115     'vbahelper.msforms'                 : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   5.2
116     'vbahelper.vbahelper'               : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
117     'vcl.vcl'                           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  35.7
118     'writerfilter.writerfilter'         : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #  19.7/27.3
119     'xmloff.xo'                         : ( 7, EXCLUDE, INCLUDE, INCLUDE), #  22.1
120     'xmloff.xof'                        : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #   4.4
121     'xmlscript.xmlscript'               : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #   3.6
122     'xmlsecurity.xmlsecurity'           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   5.1
123     'xmlsecurity.xsec_xmlsec'           : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.4
124     'xmlsecurity.xsec_gpg'              : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   ?
127 def remove_rare(raw, min_use=-1):
128     """ Remove headers not commonly included.
129         The minimum threshold is min_use.
130     """
131     # The minimum number of times a header
132     # must be included to be in the PCH.
133     min_use = min_use if min_use >= 0 else CUTOFF
135     out = []
136     if not raw or not len(raw):
137         return out
139     inc = sorted(raw)
140     last = inc[0]
141     count = 1
142     for x in range(1, len(inc)):
143         i = inc[x]
144         if i == last:
145             count += 1
146         else:
147             if count >= min_use:
148                 out.append(last)
149             last = i
150             count = 1
152     # Last group.
153     if count >= min_use:
154         out.append(last)
156     return out
158 def process_list(list, callable):
159     """ Given a list and callable
160         we pass each entry through
161         the callable and only add to
162         the output if not blank.
163     """
164     out = []
165     for i in list:
166         line = callable(i)
167         if line and len(line):
168             out.append(line)
169     return out
171 def find_files(path, recurse=True):
172     list = []
173     for root, dir, files in os.walk(path):
174         list += map(lambda x: os.path.join(root, x), files)
175     return list
177 def get_filename(line):
178     """ Strips the line from the
179         '#include' and angled brackets
180         and return the filename only.
181     """
182     if not len(line) or line[0] != '#':
183         return line
184     return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
186 def is_c_runtime(inc, root, module):
187     """ Heuristic-based detection of C/C++
188         runtime headers.
189         They are all-lowercase, with .h or
190         no extension, filename only.
191         Try to check that they are not LO headers.
192     """
193     inc = get_filename(inc)
195     if inc.endswith('.hxx') or inc.endswith('.hpp'):
196         return False
198     if inc.endswith('.h') and inc.startswith( 'config_' ):
199         return False
201     hasdot = False
202     for c in inc:
203         if c == '/':
204             return False
205         if c == '.' and not inc.endswith('.h'):
206             return False
207         if c == '.':
208             hasdot = True
209         if c.isupper():
210             return False
211     if not hasdot: # <memory> etc.
212         return True
214     if glob.glob(os.path.join(root, module, '**', inc), recursive=True):
215         return False;
217     return True
219 def sanitize(raw):
220     """ There are two forms of includes,
221         those with <> and "".
222         Technically, the difference is that
223         the compiler can use an internal
224         representation for an angled include,
225         such that it doesn't have to be a file.
226         For our purposes, there is no difference.
227         Here, we convert everything to angled.
228     """
229     if not raw or not len(raw):
230         return ''
231     raw = raw.strip()
232     if not len(raw):
233         return ''
234     return re.sub(r'(.*#include\s*)\"(.*)\"(.*)', r'#include <\2>', raw)
236 class Filter_Local(object):
237     """ Filter headers local to a module.
238         allow_public: allows include/module/file.hxx
239                       #include <module/file.hxx>
240         allow_module: allows module/inc/file.hxx
241                       #include <file.hxx>
242         allow_locals: allows module/source/file.hxx and
243                              module/source/inc/file.hxx
244                       #include <file.hxx>
245     """
246     def __init__(self, root, module, allow_public=True, allow_module=True, allow_locals=True):
247         self.root = root
248         self.module = module
249         self.allow_public = allow_public
250         self.allow_module = allow_module
251         self.allow_locals = allow_locals
252         self.public_prefix = '<' + self.module + '/'
254         all = find_files(os.path.join(root, module))
255         self.module_includes = []
256         self.locals = []
257         mod_prefix = module + '/inc/'
258         for i in all:
259             if mod_prefix in i:
260                 self.module_includes.append(i)
261             else:
262                 self.locals.append(i)
264     def is_public(self, line):
265         return self.public_prefix in line
267     def is_module(self, line):
268         """ Returns True if in module/inc/... """
269         filename = get_filename(line)
270         for i in self.module_includes:
271             if i.endswith(filename):
272                 return True
273         return False
275     def is_local(self, line):
276         """ Returns True if in module/source/... """
277         filename = get_filename(line)
278         for i in self.locals:
279             if i.endswith(filename):
280                 return True
281         return False
283     def is_external(self, line):
284         return is_c_runtime(line, self.root, self.module) and \
285                not self.is_public(line) and \
286                not self.is_module(line) and \
287                not self.is_local(line)
289     def find_local_file(self, line):
290         """ Finds the header file in the module dir,
291             but doesn't validate.
292         """
293         filename = get_filename(line)
294         for i in self.locals:
295             if i.endswith(filename):
296                 return i
297         for i in self.module_includes:
298             if i.endswith(filename):
299                 return i
300         return None
302     def proc(self, line):
303         assert line and len(line)
305         if line[0] == '#':
306             if not SILENT:
307                 sys.stderr.write('unhandled #include : {}\n'.format(line))
308             return ''
310         assert line[0] != '<' and line[0] != '#'
312         filename = get_filename(line)
314         # Local with relative path.
315         if filename.startswith('..'):
316             # Exclude for now as we don't have cxx path.
317             return ''
319         # Locals are included first (by the compiler).
320         if self.is_local(filename):
321             # Use only locals that are in some /inc/ directory (either in <module>/inc or
322             # somewhere under <module>/source/**/inc/, compilations use -I for these paths
323             # and headers elsewhere would not be found when compiling the PCH.
324             if not self.allow_locals:
325                 return ''
326             elif '/inc/' in filename:
327                 return filename
328             elif glob.glob(os.path.join(self.root, self.module, '**', 'inc', filename), recursive=True):
329                 return filename
330             else:
331                 return ''
333         # Module headers are next.
334         if self.is_module(filename):
335             return line if self.allow_module else ''
337         # Public headers are last.
338         if self.is_public(line):
339             return line if self.allow_public else ''
341         # Leave out potentially unrelated files local
342         # to some other module we can't include directly.
343         if '/' not in filename and not self.is_external(filename):
344             return ''
346         # Unfiltered.
347         return line
349 def filter_ignore(line, module):
350     """ Filters includes from known
351         problematic ones.
352         Expects sanitized input.
353     """
354     assert line and len(line)
356     # Always include files without extension.
357     if '.' not in line:
358         return line
360     # Extract filenames for ease of comparison.
361     line = get_filename(line)
363     # Filter out all files that are not normal headers.
364     if not line.endswith('.h') and \
365        not line.endswith('.hxx') and \
366        not line.endswith('.hpp') and \
367        not line.endswith('.hdl'):
368        return ''
370     ignore_list = [
371             'LibreOfficeKit/LibreOfficeKitEnums.h', # Needs special directives
372             'LibreOfficeKit/LibreOfficeKitTypes.h', # Needs special directives
373             'jerror.h',     # c++ unfriendly
374             'jpeglib.h',    # c++ unfriendly
375             'boost/spirit/include/classic_core.hpp' # depends on BOOST_SPIRIT_DEBUG
376         ]
378     if module == 'basic':
379         ignore_list += [
380             'basic/vbahelper.hxx',
381             ]
382     if module == 'connectivity':
383         ignore_list += [
384             'com/sun/star/beans/PropertyAttribute.hpp', # OPTIONAL defined via objbase.h
385             'com/sun/star/sdbcx/Privilege.hpp', # DELETE defined via objbase.h
386             'ado/*' , # some strange type conflict because of Window's adoctint.h
387             'adoint.h',
388             'adoctint.h',
389             ]
390     if module == 'sc':
391         ignore_list += [
392             'progress.hxx', # special directives
393             'scslots.hxx',  # special directives
394            ]
395     if module == 'sd':
396         ignore_list += [
397             'sdgslots.hxx', # special directives
398             'sdslots.hxx',  # special directives
399            ]
400     if module == 'sfx2':
401         ignore_list += [
402             'sfx2/recentdocsview.hxx', # Redefines ApplicationType defined in objidl.h
403             'sfx2/sidebar/Sidebar.hxx',
404             'sfx2/sidebar/UnoSidebar.hxx',
405             'sfxslots.hxx', # externally defined types
406             ]
407     if module == 'sot':
408         ignore_list += [
409             'sysformats.hxx',   # Windows headers
410             ]
411     if module == 'vcl':
412         ignore_list += [
413             'accmgr.hxx',   # redefines ImplAccelList
414             'image.h',
415             'jobset.h',
416             'opengl/gdiimpl.hxx',
417             'opengl/salbmp.hxx',
418             'openglgdiimpl',   # ReplaceTextA
419             'printdlg.hxx',
420             'salinst.hxx',  # GetDefaultPrinterA
421             'salprn.hxx',   # SetPrinterDataA
422             'vcl/jobset.hxx',
423             'vcl/oldprintadaptor.hxx',
424             'vcl/opengl/OpenGLContext.hxx',
425             'vcl/opengl/OpenGLHelper.hxx',  # Conflicts with X header on *ix
426             'vcl/print.hxx',
427             'vcl/prntypes.hxx', # redefines Orientation from filter/jpeg/Exif.hxx
428             'vcl/sysdata.hxx',
429             ]
430     if module == 'xmloff':
431         ignore_list += [
432             'SchXMLExport.hxx', # SchXMLAutoStylePoolP.hxx not found
433             'SchXMLImport.hxx', # enums redefined in draw\sdxmlimp_impl.hxx
434             'XMLEventImportHelper.hxx', # NameMap redefined in XMLEventExport.hxx
435             'xmloff/XMLEventExport.hxx', # enums redefined
436             ]
437     if module == 'xmlsecurity':
438         ignore_list += [
439             'xmlsec/*',
440             'xmlsecurity/xmlsec-wrapper.h',
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_SUPPORT_GPU":
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 #if !defined _CRT_RAND_S
892 #define _CRT_RAND_S
893 #endif
894 #endif
896             f.write(sal_define)
898         # Dump the headers.
899         f.write('\n')
900         for i in includes:
901             f.write(i + '\n')
903         # Some libraries pull windows headers that aren't self contained.
904         if (module == 'connectivity' and libname == 'ado') or \
905            (module == 'xmlsecurity' and libname == 'xsec_xmlsec'):
906             ado_define = """
907 // Cleanup windows header macro pollution.
908 #if defined(_WIN32) && defined(WINAPI)
909 #include <postwin.h>
910 #undef RGB
911 #endif
913             f.write(ado_define)
915         f.write(footer)
917 def remove_from_tree(filename, tree):
918     # Remove this file, if top-level.
919     incs = tree.pop(filename, [])
920     for i in incs:
921         tree = remove_from_tree(i, tree)
923     # Also remove if included from another.
924     for (k, v) in tree.items():
925         if filename in v:
926             v.remove(filename)
928     return tree
930 def tree_to_list(includes, filename, tree):
931     if filename in includes:
932         return includes
933     includes.append(filename)
934     #incs = tree.pop(filename, [])
935     incs = tree[filename] if filename in tree else []
936     for i in incs:
937         tree_to_list(includes, i, tree)
939     return includes
941 def promote(includes):
942     """ Common library headers are heavily
943         referenced, even if they are included
944         from a few places.
945         Here we separate them to promote
946         their inclusion in the final pch.
947     """
948     promo = []
949     for inc in includes:
950         if inc.startswith('boost') or \
951            inc.startswith('sal') or \
952            inc.startswith('osl') or \
953            inc.startswith('rtl'):
954             promo.append(inc)
955     return promo
957 def make_pch_filename(root, module, libname):
958     """ PCH files are stored here:
959         <root>/<module>/inc/pch/precompiled_<libname>.hxx
960     """
962     path = os.path.join(root, module)
963     path = os.path.join(path, 'inc')
964     path = os.path.join(path, 'pch')
965     path = os.path.join(path, 'precompiled_' + libname + '.hxx')
966     return path
968 def main():
970     global CUTOFF
971     global EXCLUDE_MODULE
972     global EXCLUDE_LOCAL
973     global EXCLUDE_SYSTEM
974     global SILENT
975     global WORKDIR
977     if os.getenv('WORKDIR'):
978         WORKDIR = os.getenv('WORKDIR')
980     root = '.'
981     module = sys.argv[1]
982     libname = sys.argv[2]
983     header = make_pch_filename(root, module, libname)
985     if not os.path.exists(os.path.join(root, module)):
986         raise Exception('Error: module [{}] not found.'.format(module))
988     key = '{}.{}'.format(module, libname)
989     if key in DEFAULTS:
990         # Load the module-specific defaults.
991         CUTOFF = DEFAULTS[key][0]
992         EXCLUDE_SYSTEM = DEFAULTS[key][1]
993         EXCLUDE_MODULE = DEFAULTS[key][2]
994         EXCLUDE_LOCAL = DEFAULTS[key][3]
996     force_update = False
997     for x in range(3, len(sys.argv)):
998         i = sys.argv[x]
999         if i.startswith('--cutoff='):
1000             CUTOFF = int(i.split('=')[1])
1001         elif i.startswith('--exclude:'):
1002             cat = i.split(':')[1]
1003             if cat == 'module':
1004                 EXCLUDE_MODULE = True
1005             elif cat == 'local':
1006                 EXCLUDE_LOCAL = True
1007             elif cat == 'system':
1008                 EXCLUDE_SYSTEM = True
1009         elif i.startswith('--include:'):
1010             cat = i.split(':')[1]
1011             if cat == 'module':
1012                 EXCLUDE_MODULE = False
1013             elif cat == 'local':
1014                 EXCLUDE_LOCAL = False
1015             elif cat == 'system':
1016                 EXCLUDE_SYSTEM = False
1017         elif i == '--silent':
1018             SILENT = True
1019         elif i == '--force':
1020             force_update = True
1021         else:
1022             sys.stderr.write('Unknown option [{}].'.format(i))
1023             return 1
1025     filter_local = Filter_Local(root, module, \
1026                                 not EXCLUDE_MODULE, \
1027                                 not EXCLUDE_LOCAL)
1029     # Read input.
1030     groups = process_makefile(root, module, libname)
1032     generic = []
1033     for osname, group in groups.items():
1034         if not len(group):
1035             continue
1037         includes = []
1038         for filename in group:
1039             includes += process_source(root, module, filename)
1041         # Save unique top-level includes.
1042         unique = set(includes)
1043         promoted = promote(unique)
1045         # Process includes.
1046         includes = remove_rare(includes)
1047         includes = process_list(includes, lambda x: filter_ignore(x, module))
1048         includes = process_list(includes, filter_local.proc)
1050         # Remove the already included ones.
1051         for inc in includes:
1052             unique.discard(inc)
1054         # Explode the excluded ones.
1055         tree = {i:[] for i in includes}
1056         tree = explode(root, module, unique, tree, filter_local, not EXCLUDE_MODULE)
1058         # Remove the already included ones from the tree.
1059         for inc in includes:
1060             filename = get_filename(inc)
1061             tree = remove_from_tree(filename, tree)
1063         extra = []
1064         for (k, v) in tree.items():
1065             extra += tree_to_list([], k, tree)
1067         promoted += promote(extra)
1068         promoted = process_list(promoted, lambda x: filter_ignore(x, module))
1069         promoted = process_list(promoted, filter_local.proc)
1070         promoted = set(promoted)
1071         # If a promoted header includes others, remove the rest.
1072         for (k, v) in tree.items():
1073             if k in promoted:
1074                 for i in v:
1075                     promoted.discard(i)
1076         includes += [x for x in promoted]
1078         extra = remove_rare(extra)
1079         extra = process_list(extra, lambda x: filter_ignore(x, module))
1080         extra = process_list(extra, filter_local.proc)
1081         includes += extra
1083         includes = [x for x in set(includes)]
1084         fixes = fixup(includes, module)
1085         fixes = map(lambda x: '#include <' + x + '>', fixes)
1087         includes = map(lambda x: '#include <' + x + '>', includes)
1088         sorted = sort_by_category(includes, root, module, filter_local)
1089         includes = list(fixes) + sorted
1091         if len(osname):
1092             for i in generic:
1093                 if i in includes:
1094                     includes.remove(i)
1096         groups[osname] = includes
1097         if not len(osname):
1098             generic = includes
1100     # Open the old pch and compare its contents
1101     # with new includes.
1102     # Clobber only if they are different.
1103     with open(header, 'r') as f:
1104         old_pch_lines = [x.strip() for x in f.readlines()]
1105         new_lines = generate_includes(groups)
1106         # Find the first include in the old pch.
1107         start = -1
1108         for i in range(len(old_pch_lines)):
1109             if old_pch_lines[i].startswith('#include') or old_pch_lines[i].startswith('#if PCH_LEVEL'):
1110                 start = i
1111                 break
1112         # Clobber if there is a mismatch.
1113         if force_update or start < 0 or (len(old_pch_lines) - start < len(new_lines)):
1114             generate(new_lines, libname, header, module)
1115             return 0
1116         else:
1117             for i in range(len(new_lines)):
1118                 if new_lines[i] != old_pch_lines[start + i]:
1119                     generate(new_lines, libname, header, module)
1120                     return 0
1121             else:
1122                 # Identical, but see if new pch removed anything.
1123                 for i in range(start + len(new_lines), len(old_pch_lines)):
1124                     if '#include' in old_pch_lines[i]:
1125                         generate(new_lines, libname, header, module)
1126                         return 0
1128     # Didn't update.
1129     # Use exit code 2 to distinguish it from exit code 1 used e.g. when an exception occurs.
1130     return 2
1132 if __name__ == '__main__':
1133     """ Process all the includes in a Module
1134         to make into a PCH file.
1135         Run without arguments for unittests,
1136         and to see usage.
1137     """
1139     if len(sys.argv) >= 3:
1140         status = main()
1141         sys.exit(status)
1143     print('Usage: {} <Module name> <Library name> [options]'.format(sys.argv[0]))
1144     print('    Always run from the root of LO repository.\n')
1145     print('    Options:')
1146     print('    --cutoff=<count> - Threshold to excluding headers.')
1147     print('    --exclude:<category> - Exclude category-specific headers.')
1148     print('    --include:<category> - Include category-specific headers.')
1149     print('    --force - Force updating the pch even when nothing changes.')
1150     print('    Categories:')
1151     print('         module - Headers in /inc directory of a module.')
1152     print('         local  - Headers local to a source file.')
1153     print('         system - Platform-specific headers.')
1154     print('    --silent - print only errors.')
1155     print('\nRunning unit-tests...')
1158 class TestMethods(unittest.TestCase):
1160     def test_sanitize(self):
1161         self.assertEqual(sanitize('#include "blah/file.cxx"'),
1162                                 '#include <blah/file.cxx>')
1163         self.assertEqual(sanitize('  #include\t"blah/file.cxx" '),
1164                                 '#include <blah/file.cxx>')
1165         self.assertEqual(sanitize('  '),
1166                                 '')
1168     def test_filter_ignore(self):
1169         self.assertEqual(filter_ignore('blah/file.cxx', 'mod'),
1170                                      '')
1171         self.assertEqual(filter_ignore('vector', 'mod'),
1172                                      'vector')
1173         self.assertEqual(filter_ignore('file.cxx', 'mod'),
1174                                      '')
1176     def test_remove_rare(self):
1177         self.assertEqual(remove_rare([]),
1178                                 [])
1180 class TestMakefileParser(unittest.TestCase):
1182     def setUp(self):
1183         global EXCLUDE_SYSTEM
1184         EXCLUDE_SYSTEM = False
1186     def test_parse_singleline_eval(self):
1187         source = "$(eval $(call gb_Library_Library,sal))"
1188         lines = source.split('\n')
1189         groups = {'':[]}
1190         groups = parse_makefile(groups, lines, 0, None, 0)
1191         self.assertEqual(len(groups), 1)
1192         self.assertEqual(len(groups['']), 0)
1194     def test_parse_multiline_eval(self):
1195         source = """$(eval $(call gb_Library_set_include,sal,\\
1196         $$(INCLUDE) \\
1197         -I$(SRCDIR)/sal/inc \\
1200         lines = source.split('\n')
1201         groups = {'':[]}
1202         groups = parse_makefile(groups, lines, 0, None, 0)
1203         self.assertEqual(len(groups), 1)
1204         self.assertEqual(len(groups['']), 0)
1206     def test_parse_multiline_eval_with_if(self):
1207         source = """$(eval $(call gb_Library_add_defs,sal,\\
1208         $(if $(filter $(OS),iOS), \\
1209                 -DNO_CHILD_PROCESSES \\
1210         ) \\
1213         lines = source.split('\n')
1214         groups = {'':[]}
1215         groups = parse_makefile(groups, lines, 0, None, 0)
1216         self.assertEqual(len(groups), 1)
1217         self.assertEqual(len(groups['']), 0)
1219     def test_parse_multiline_add_with_if(self):
1220         source = """$(eval $(call gb_Library_add_exception_objects,sal,\\
1221         sal/osl/unx/time \\
1222         $(if $(filter DESKTOP,$(BUILD_TYPE)), sal/osl/unx/salinit) \\
1225         lines = source.split('\n')
1226         groups = {'':[]}
1227         groups = parse_makefile(groups, lines, 0, None, 0)
1228         self.assertEqual(len(groups), 1)
1229         self.assertEqual(len(groups['']), 1)
1230         self.assertEqual(groups[''][0], 'sal/osl/unx/time.cxx')
1232     def test_parse_if_else(self):
1233         source = """ifeq ($(OS),MACOSX)
1234 $(eval $(call gb_Library_add_exception_objects,sal,\\
1235         sal/osl/mac/mac \\
1237 else
1238 $(eval $(call gb_Library_add_exception_objects,sal,\\
1239         sal/osl/unx/uunxapi \\
1241 endif
1243         lines = source.split('\n')
1244         groups = {'':[]}
1245         groups = parse_makefile(groups, lines, 0, None, 0)
1246         self.assertEqual(len(groups), 3)
1247         self.assertEqual(len(groups['']), 0)
1248         self.assertEqual(len(groups['MACOSX']), 1)
1249         self.assertEqual(len(groups['!MACOSX']), 1)
1250         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1251         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1253     def test_parse_nested_if(self):
1254         source = """ifeq ($(OS),MACOSX)
1255 $(eval $(call gb_Library_add_exception_objects,sal,\\
1256         sal/osl/mac/mac \\
1258 else
1259 $(eval $(call gb_Library_add_exception_objects,sal,\\
1260         sal/osl/unx/uunxapi \\
1263 ifeq ($(OS),LINUX)
1264 $(eval $(call gb_Library_add_exception_objects,sal,\\
1265         sal/textenc/context \\
1267 endif
1268 endif
1270         lines = source.split('\n')
1271         groups = {'':[]}
1272         groups = parse_makefile(groups, lines, 0, None, 0)
1273         self.assertEqual(len(groups), 4)
1274         self.assertEqual(len(groups['']), 0)
1275         self.assertEqual(len(groups['MACOSX']), 1)
1276         self.assertEqual(len(groups['!MACOSX']), 1)
1277         self.assertEqual(len(groups['LINUX']), 1)
1278         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1279         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1280         self.assertEqual(groups['LINUX'][0], 'sal/textenc/context.cxx')
1282     def test_parse_exclude_system(self):
1283         source = """ifeq ($(OS),MACOSX)
1284 $(eval $(call gb_Library_add_exception_objects,sal,\\
1285         sal/osl/mac/mac \\
1287 else
1288 $(eval $(call gb_Library_add_exception_objects,sal,\\
1289         sal/osl/unx/uunxapi \\
1292 ifeq ($(OS),LINUX)
1293 $(eval $(call gb_Library_add_exception_objects,sal,\\
1294         sal/textenc/context \\
1296 endif
1297 endif
1299         global EXCLUDE_SYSTEM
1300         EXCLUDE_SYSTEM = True
1302         lines = source.split('\n')
1303         groups = {'':[]}
1304         groups = parse_makefile(groups, lines, 0, None, 0)
1305         self.assertEqual(len(groups), 1)
1306         self.assertEqual(len(groups['']), 0)
1308     def test_parse_filter(self):
1309         source = """ifneq ($(filter $(OS),MACOSX iOS),)
1310 $(eval $(call gb_Library_add_exception_objects,sal,\\
1311         sal/osl/unx/osxlocale \\
1313 endif
1315         # Filter is still unsupported.
1316         lines = source.split('\n')
1317         groups = {'':[]}
1318         groups = parse_makefile(groups, lines, 0, None, 0)
1319         self.assertEqual(len(groups), 1)
1320         self.assertEqual(len(groups['']), 0)
1322 unittest.main()
1324 # vim: set et sw=4 ts=4 expandtab: