tdf#118711 writerfilter: don't hardcode default page description
[LibreOffice.git] / bin / update_pch
blobdca39eac8bea7d422fdf677ce54f51701b6c8853
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 brakets
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             ]
479     for i in ignore_list:
480         if line.startswith(i):
481             return ''
482         if i[0] == '*' and line.endswith(i[1:]):
483             return ''
484         if i[-1] == '*' and line.startswith(i[:-1]):
485             return ''
487     return line
489 def fixup(includes, module):
490     """ Here we add any headers
491         necessary in the pch.
492         These could be known to be very
493         common but for technical reasons
494         left out of the pch by this generator.
495         Or, they could be missing from the
496         source files where they are used
497         (probably because they had been
498         in the old pch, they were missed).
499         Also, these could be headers
500         that make the build faster but
501         aren't added automatically.
502     """
503     fixes = []
504     def append(inc):
505         # Add a space to exclude from
506         # ignore bisecting.
507         line = ' #include <{}>'.format(inc)
508         try:
509             i = fixes.index(inc)
510             fixes[i] = inc
511         except:
512             fixes.append(inc)
514     if module == 'basctl':
515         if 'basslots.hxx' in includes:
516             append('sfx2/msg.hxx')
518     #if module == 'sc':
519     #    if 'scslots.hxx' in includes:
520     #        append('sfx2/msg.hxx')
521     return fixes
523 def sort_by_category(list, root, module, filter_local):
524     """ Move all 'system' headers first.
525         Core files of osl, rtl, sal, next.
526         Everything non-module-specific third.
527         Last, module-specific headers.
528     """
529     sys = []
530     boo = []
531     cor = []
532     rst = []
533     mod = []
535     prefix = '<' + module + '/'
536     for i in list:
537         if is_c_runtime(i, root, module):
538             sys.append(i)
539         elif '<boost/' in i:
540             boo.append(i)
541         elif prefix in i or not '/' in i:
542             mod.append(i)
543         elif '<sal/' in i or '<vcl/' in i:
544             cor.append(i)
545         elif '<osl/' in i or '<rtl/' in i:
546             if module == "sal": # osl and rtl are also part of sal
547                 mod.append(i)
548             else:
549                 cor.append(i)
550         # Headers from another module that is closely tied to the module.
551         elif module == 'sc' and '<formula' in i:
552             mod.append(i)
553         else:
554             rst.append(i)
556     out = []
557     out += [ "#if PCH_LEVEL >= 1" ]
558     out += sorted(sys)
559     out += sorted(boo)
560     out += [ "#endif // PCH_LEVEL >= 1" ]
561     out += [ "#if PCH_LEVEL >= 2" ]
562     out += sorted(cor)
563     out += [ "#endif // PCH_LEVEL >= 2" ]
564     out += [ "#if PCH_LEVEL >= 3" ]
565     out += sorted(rst)
566     out += [ "#endif // PCH_LEVEL >= 3" ]
567     out += [ "#if PCH_LEVEL >= 4" ]
568     out += sorted(mod)
569     out += [ "#endif // PCH_LEVEL >= 4" ]
570     return out
572 def parse_makefile(groups, lines, lineno, lastif, ifstack):
574     inobjects = False
575     ingeneratedobjects = False
576     inelse = False
577     suffix = 'cxx'
578     os_cond_re = re.compile(r'(ifeq|ifneq)\s*\(\$\(OS\)\,(\w*)\)')
580     line = lines[lineno]
581     if line.startswith('if'):
582         lastif = line
583         if ifstack == 0:
584             # Correction if first line is an if.
585             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
586     else:
587         lineno -= 1
589     while lineno + 1 < len(lines):
590         lineno += 1
591         line = lines[lineno].strip()
592         line = line.rstrip('\\').strip()
593         #print('line #{}: {}'.format(lineno, line))
594         if len(line) == 0:
595             continue
597         if line == '))':
598             inobjects = False
599             ingeneratedobjects = False
600         elif 'add_exception_objects' in line or \
601              'add_cxxobject' in line:
602              inobjects = True
603              #print('inobjects')
604              #if ifstack and not SILENT:
605                 #sys.stderr.write('Sources in a conditional, ignoring for now.\n')
606         elif 'add_generated_exception_objects' in line or \
607              'add_generated_cxxobject' in line:
608              ingeneratedobjects = True
609         elif 'set_generated_cxx_suffix' in line:
610             suffix_re = re.compile('.*set_generated_cxx_suffix,[^,]*,([^)]*).*')
611             match = suffix_re.match(line)
612             if match:
613                 suffix = match.group(1)
614         elif line.startswith('if'):
615             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
616             continue
617         elif line.startswith('endif'):
618             if ifstack:
619                 return lineno
620             continue
621         elif line.startswith('else'):
622             inelse = True
623         elif inobjects or ingeneratedobjects:
624             if EXCLUDE_SYSTEM and ifstack:
625                 continue
626             file = line + '.' + suffix
627             if ',' in line or '(' in line or ')' in line or file.startswith('-'):
628                 #print('passing: ' + line)
629                 pass # $if() probably, or something similar
630             else:
631                 osname = ''
632                 if lastif:
633                     if 'filter' in lastif:
634                         # We can't grok filter, yet.
635                         continue
636                     match = os_cond_re.match(lastif)
637                     if not match:
638                         # We only support OS conditionals.
639                         continue
640                     in_out = match.group(1)
641                     osname = match.group(2) if match else ''
642                     if (in_out == 'ifneq' and not inelse) or \
643                        (in_out == 'ifeq' and inelse):
644                         osname = '!' + osname
646                 if osname not in groups:
647                     groups[osname] = []
648                 if ingeneratedobjects:
649                     file = WORKDIR + '/' + file
650                 groups[osname].append(file)
652     return groups
654 def process_makefile(root, module, libname):
655     """ Parse a gmake makefile and extract
656         source filenames from it.
657     """
659     makefile = 'Library_{}.mk'.format(libname)
660     filename = os.path.join(os.path.join(root, module), makefile)
661     if not os.path.isfile(filename):
662         makefile = 'StaticLibrary_{}.mk'.format(libname)
663         filename = os.path.join(os.path.join(root, module), makefile)
664         if not os.path.isfile(filename):
665             sys.stderr.write('Error: Module {} has no makefile at {}.'.format(module, filename))
667     groups = {'':[], 'ANDROID':[], 'iOS':[], 'WNT':[], 'LINUX':[], 'MACOSX':[]}
669     with open(filename, 'r') as f:
670         lines = f.readlines()
671         groups = parse_makefile(groups, lines, lineno=0, lastif=None, ifstack=0)
673     return groups
675 def is_allowed_if(line, module):
676     """ Check whether the given #if condition
677         is allowed for the given module or whether
678         its block should be ignored.
679     """
681     # remove trailing comments
682     line = re.sub(r'(.*) *//.*', r'\1', line)
683     line = line.strip()
685     # Our sources always build with LIBO_INTERNAL_ONLY.
686     if line == "#if defined LIBO_INTERNAL_ONLY" or line == "#ifdef LIBO_INTERNAL_ONLY":
687         return True
688     if module == "external/skia":
689         # We always set these.
690         if line == "#ifdef SK_VULKAN" or line == "#if SK_SUPPORT_GPU":
691             return True
692     return False
694 def process_source(root, module, filename, maxdepth=0):
695     """ Process a source file to extract
696         included headers.
697         For now, skip on compiler directives.
698         maxdepth is used when processing headers
699         which typically have protecting ifndef.
700     """
702     ifdepth = 0
703     lastif = ''
704     raw_includes = []
705     allowed_ifs = []
706     ifsallowed = 0
707     with open(filename, 'r') as f:
708         for line in f:
709             line = line.strip()
710             if line.startswith('#if'):
711                 if is_allowed_if(line, module):
712                     allowed_ifs.append(True)
713                     ifsallowed += 1
714                 else:
715                     allowed_ifs.append(False)
716                     lastif = line
717                 ifdepth += 1
718             elif line.startswith('#endif'):
719                 ifdepth -= 1
720                 if allowed_ifs[ ifdepth ]:
721                     ifsallowed -= 1
722                 else:
723                     lastif = '#if'
724                 del allowed_ifs[ ifdepth ]
725             elif line.startswith('#include'):
726                 if ifdepth - ifsallowed <= maxdepth:
727                     line = sanitize(line)
728                     if line:
729                         line = get_filename(line)
730                     if line and len(line):
731                         raw_includes.append(line)
732                 elif not SILENT:
733                     sys.stderr.write('#include in {} : {}\n'.format(lastif, line))
735     return raw_includes
737 def explode(root, module, includes, tree, filter_local, recurse):
738     incpath = os.path.join(root, 'include')
740     for inc in includes:
741         filename = get_filename(inc)
742         if filename in tree or len(filter_local.proc(filename)) == 0:
743             continue
745         try:
746             # Module or Local header.
747             filepath = filter_local.find_local_file(inc)
748             if filepath:
749                 #print('trying loc: ' + filepath)
750                 incs = process_source(root, module, filepath, maxdepth=1)
751                 incs = map(get_filename, incs)
752                 incs = process_list(incs, lambda x: filter_ignore(x, module))
753                 incs = process_list(incs, filter_local.proc)
754                 tree[filename] = incs
755                 if recurse:
756                     tree = explode(root, module, incs, tree, filter_local, recurse)
757                 #print('{} => {}'.format(filepath, tree[filename]))
758                 continue
759         except:
760             pass
762         try:
763             # Public header.
764             filepath = os.path.join(incpath, filename)
765             #print('trying pub: ' + filepath)
766             incs = process_source(root, module, filepath, maxdepth=1)
767             incs = map(get_filename, incs)
768             incs = process_list(incs, lambda x: filter_ignore(x, module))
769             incs = process_list(incs, filter_local.proc)
770             tree[filename] = incs
771             if recurse:
772                 tree = explode(root, module, incs, tree, filter_local, recurse)
773             #print('{} => {}'.format(filepath, tree[filename]))
774             continue
775         except:
776             pass
778         # Failed, but remember to avoid searching again.
779         tree[filename] = []
781     return tree
783 def make_command_line():
784     args = sys.argv[:]
785     # Remove command line flags and
786     # use internal flags.
787     for i in range(len(args)-1, 0, -1):
788         if args[i].startswith('--'):
789             args.pop(i)
791     args.append('--cutoff=' + str(CUTOFF))
792     if EXCLUDE_SYSTEM:
793         args.append('--exclude:system')
794     else:
795         args.append('--include:system')
796     if EXCLUDE_MODULE:
797         args.append('--exclude:module')
798     else:
799         args.append('--include:module')
800     if EXCLUDE_LOCAL:
801         args.append('--exclude:local')
802     else:
803         args.append('--include:local')
805     return ' '.join(args)
807 def generate_includes(includes):
808     """Generates the include lines of the pch.
809     """
810     lines = []
811     for osname, group in includes.items():
812         if not len(group):
813             continue
815         if len(osname):
816             not_eq = ''
817             if osname[0] == '!':
818                 not_eq = '!'
819                 osname = osname[1:]
820             lines.append('')
821             lines.append('#if {}defined({})'.format(not_eq, osname))
823         for i in group:
824             lines.append(i)
826         if len(osname):
827             lines.append('#endif')
829     return lines
831 def generate(includes, libname, filename, module):
832     header = \
833 """/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
835  * This file is part of the LibreOffice project.
837  * This Source Code Form is subject to the terms of the Mozilla Public
838  * License, v. 2.0. If a copy of the MPL was not distributed with this
839  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
840  */
843  This file has been autogenerated by update_pch.sh. It is possible to edit it
844  manually (such as when an include file has been moved/renamed/removed). All such
845  manual changes will be rewritten by the next run of update_pch.sh (which presumably
846  also fixes all possible problems, so it's usually better to use it).
849     footer = \
851 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
853     import datetime
855     with open(filename, 'w') as f:
856         f.write(header)
857         f.write('\n Generated on {} using:\n {}\n'.format(
858                 datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
859                 make_command_line()))
860         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(
861                 filename, module))
863         # sal needs this for rand_s()
864         if module == 'sal' and libname == 'sal':
865             sal_define = """
866 #if defined(_WIN32)
867 #if !defined _CRT_RAND_S
868 #define _CRT_RAND_S
869 #endif
870 #endif
872             f.write(sal_define)
874         # Dump the headers.
875         f.write('\n')
876         for i in includes:
877             f.write(i + '\n')
879         # Some libraries pull windows headers that aren't self contained.
880         if (module == 'connectivity' and libname == 'ado') or \
881            (module == 'xmlsecurity' and libname == 'xsec_xmlsec'):
882             ado_define = """
883 // Cleanup windows header macro pollution.
884 #if defined(_WIN32) && defined(WINAPI)
885 #include <postwin.h>
886 #undef RGB
887 #endif
889             f.write(ado_define)
891         f.write(footer)
893 def remove_from_tree(filename, tree):
894     # Remove this file, if top-level.
895     incs = tree.pop(filename, [])
896     for i in incs:
897         tree = remove_from_tree(i, tree)
899     # Also remove if included from another.
900     for (k, v) in tree.items():
901         if filename in v:
902             v.remove(filename)
904     return tree
906 def tree_to_list(includes, filename, tree):
907     if filename in includes:
908         return includes
909     includes.append(filename)
910     #incs = tree.pop(filename, [])
911     incs = tree[filename] if filename in tree else []
912     for i in incs:
913         tree_to_list(includes, i, tree)
915     return includes
917 def promote(includes):
918     """ Common library headers are heavily
919         referenced, even if they are included
920         from a few places.
921         Here we separate them to promote
922         their inclusion in the final pch.
923     """
924     promo = []
925     for inc in includes:
926         if inc.startswith('boost') or \
927            inc.startswith('sal') or \
928            inc.startswith('osl') or \
929            inc.startswith('rtl'):
930             promo.append(inc)
931     return promo
933 def make_pch_filename(root, module, libname):
934     """ PCH files are stored here:
935         <root>/<module>/inc/pch/precompiled_<libname>.hxx
936     """
938     path = os.path.join(root, module)
939     path = os.path.join(path, 'inc')
940     path = os.path.join(path, 'pch')
941     path = os.path.join(path, 'precompiled_' + libname + '.hxx')
942     return path
944 def main():
946     global CUTOFF
947     global EXCLUDE_MODULE
948     global EXCLUDE_LOCAL
949     global EXCLUDE_SYSTEM
950     global SILENT
951     global WORKDIR
953     if os.getenv('WORKDIR'):
954         WORKDIR = os.getenv('WORKDIR')
956     root = '.'
957     module = sys.argv[1]
958     libname = sys.argv[2]
959     header = make_pch_filename(root, module, libname)
961     if not os.path.exists(os.path.join(root, module)):
962         raise Exception('Error: module [{}] not found.'.format(module))
964     key = '{}.{}'.format(module, libname)
965     if key in DEFAULTS:
966         # Load the module-specific defaults.
967         CUTOFF = DEFAULTS[key][0]
968         EXCLUDE_SYSTEM = DEFAULTS[key][1]
969         EXCLUDE_MODULE = DEFAULTS[key][2]
970         EXCLUDE_LOCAL = DEFAULTS[key][3]
972     force_update = False
973     for x in range(3, len(sys.argv)):
974         i = sys.argv[x]
975         if i.startswith('--cutoff='):
976             CUTOFF = int(i.split('=')[1])
977         elif i.startswith('--exclude:'):
978             cat = i.split(':')[1]
979             if cat == 'module':
980                 EXCLUDE_MODULE = True
981             elif cat == 'local':
982                 EXCLUDE_LOCAL = True
983             elif cat == 'system':
984                 EXCLUDE_SYSTEM = True
985         elif i.startswith('--include:'):
986             cat = i.split(':')[1]
987             if cat == 'module':
988                 EXCLUDE_MODULE = False
989             elif cat == 'local':
990                 EXCLUDE_LOCAL = False
991             elif cat == 'system':
992                 EXCLUDE_SYSTEM = False
993         elif i == '--silent':
994             SILENT = True
995         elif i == '--force':
996             force_update = True
997         else:
998             sys.stderr.write('Unknown option [{}].'.format(i))
999             return 1
1001     filter_local = Filter_Local(root, module, \
1002                                 not EXCLUDE_MODULE, \
1003                                 not EXCLUDE_LOCAL)
1005     # Read input.
1006     groups = process_makefile(root, module, libname)
1008     generic = []
1009     for osname, group in groups.items():
1010         if not len(group):
1011             continue
1013         includes = []
1014         for filename in group:
1015             includes += process_source(root, module, filename)
1017         # Save unique top-level includes.
1018         unique = set(includes)
1019         promoted = promote(unique)
1021         # Process includes.
1022         includes = remove_rare(includes)
1023         includes = process_list(includes, lambda x: filter_ignore(x, module))
1024         includes = process_list(includes, filter_local.proc)
1026         # Remove the already included ones.
1027         for inc in includes:
1028             unique.discard(inc)
1030         # Explode the excluded ones.
1031         tree = {i:[] for i in includes}
1032         tree = explode(root, module, unique, tree, filter_local, not EXCLUDE_MODULE)
1034         # Remove the already included ones from the tree.
1035         for inc in includes:
1036             filename = get_filename(inc)
1037             tree = remove_from_tree(filename, tree)
1039         extra = []
1040         for (k, v) in tree.items():
1041             extra += tree_to_list([], k, tree)
1043         promoted += promote(extra)
1044         promoted = process_list(promoted, lambda x: filter_ignore(x, module))
1045         promoted = process_list(promoted, filter_local.proc)
1046         promoted = set(promoted)
1047         # If a promoted header includes others, remove the rest.
1048         for (k, v) in tree.items():
1049             if k in promoted:
1050                 for i in v:
1051                     promoted.discard(i)
1052         includes += [x for x in promoted]
1054         extra = remove_rare(extra)
1055         extra = process_list(extra, lambda x: filter_ignore(x, module))
1056         extra = process_list(extra, filter_local.proc)
1057         includes += extra
1059         includes = [x for x in set(includes)]
1060         fixes = fixup(includes, module)
1061         fixes = map(lambda x: '#include <' + x + '>', fixes)
1063         includes = map(lambda x: '#include <' + x + '>', includes)
1064         sorted = sort_by_category(includes, root, module, filter_local)
1065         includes = list(fixes) + sorted
1067         if len(osname):
1068             for i in generic:
1069                 if i in includes:
1070                     includes.remove(i)
1072         groups[osname] = includes
1073         if not len(osname):
1074             generic = includes
1076     # Open the old pch and compare its contents
1077     # with new includes.
1078     # Clobber only if they are different.
1079     with open(header, 'r') as f:
1080         old_pch_lines = [x.strip() for x in f.readlines()]
1081         new_lines = generate_includes(groups)
1082         # Find the first include in the old pch.
1083         start = -1
1084         for i in range(len(old_pch_lines)):
1085             if old_pch_lines[i].startswith('#include') or old_pch_lines[i].startswith('#if PCH_LEVEL'):
1086                 start = i
1087                 break
1088         # Clobber if there is a mismatch.
1089         if force_update or start < 0 or (len(old_pch_lines) - start < len(new_lines)):
1090             generate(new_lines, libname, header, module)
1091             return 0
1092         else:
1093             for i in range(len(new_lines)):
1094                 if new_lines[i] != old_pch_lines[start + i]:
1095                     generate(new_lines, libname, header, module)
1096                     return 0
1097             else:
1098                 # Identical, but see if new pch removed anything.
1099                 for i in range(start + len(new_lines), len(old_pch_lines)):
1100                     if '#include' in old_pch_lines[i]:
1101                         generate(new_lines, libname, header, module)
1102                         return 0
1104     # Didn't update.
1105     return 1
1107 if __name__ == '__main__':
1108     """ Process all the includes in a Module
1109         to make into a PCH file.
1110         Run without arguments for unittests,
1111         and to see usage.
1112     """
1114     if len(sys.argv) >= 3:
1115         status = main()
1116         sys.exit(status)
1118     print('Usage: {} <Module name> <Library name> [options]'.format(sys.argv[0]))
1119     print('    Always run from the root of LO repository.\n')
1120     print('    Options:')
1121     print('    --cutoff=<count> - Threshold to excluding headers.')
1122     print('    --exclude:<category> - Exclude category-specific headers.')
1123     print('    --include:<category> - Include category-specific headers.')
1124     print('    --force - Force updating the pch even when nothing changes.')
1125     print('    Categories:')
1126     print('         module - Headers in /inc directory of a module.')
1127     print('         local  - Headers local to a source file.')
1128     print('         system - Platform-specific headers.')
1129     print('    --silent - print only errors.')
1130     print('\nRunning unit-tests...')
1133 class TestMethods(unittest.TestCase):
1135     def test_sanitize(self):
1136         self.assertEqual(sanitize('#include "blah/file.cxx"'),
1137                                 '#include <blah/file.cxx>')
1138         self.assertEqual(sanitize('  #include\t"blah/file.cxx" '),
1139                                 '#include <blah/file.cxx>')
1140         self.assertEqual(sanitize('  '),
1141                                 '')
1143     def test_filter_ignore(self):
1144         self.assertEqual(filter_ignore('blah/file.cxx', 'mod'),
1145                                      '')
1146         self.assertEqual(filter_ignore('vector', 'mod'),
1147                                      'vector')
1148         self.assertEqual(filter_ignore('file.cxx', 'mod'),
1149                                      '')
1151     def test_remove_rare(self):
1152         self.assertEqual(remove_rare([]),
1153                                 [])
1155 class TestMakefileParser(unittest.TestCase):
1157     def setUp(self):
1158         global EXCLUDE_SYSTEM
1159         EXCLUDE_SYSTEM = False
1161     def test_parse_singleline_eval(self):
1162         source = "$(eval $(call gb_Library_Library,sal))"
1163         lines = source.split('\n')
1164         groups = {'':[]}
1165         groups = parse_makefile(groups, lines, 0, None, 0)
1166         self.assertEqual(len(groups), 1)
1167         self.assertEqual(len(groups['']), 0)
1169     def test_parse_multiline_eval(self):
1170         source = """$(eval $(call gb_Library_set_include,sal,\\
1171         $$(INCLUDE) \\
1172         -I$(SRCDIR)/sal/inc \\
1175         lines = source.split('\n')
1176         groups = {'':[]}
1177         groups = parse_makefile(groups, lines, 0, None, 0)
1178         self.assertEqual(len(groups), 1)
1179         self.assertEqual(len(groups['']), 0)
1181     def test_parse_multiline_eval_with_if(self):
1182         source = """$(eval $(call gb_Library_add_defs,sal,\\
1183         $(if $(filter $(OS),iOS), \\
1184                 -DNO_CHILD_PROCESSES \\
1185         ) \\
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_add_with_if(self):
1195         source = """$(eval $(call gb_Library_add_exception_objects,sal,\\
1196         sal/osl/unx/time \\
1197         $(if $(filter DESKTOP,$(BUILD_TYPE)), sal/osl/unx/salinit) \\
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['']), 1)
1205         self.assertEqual(groups[''][0], 'sal/osl/unx/time.cxx')
1207     def test_parse_if_else(self):
1208         source = """ifeq ($(OS),MACOSX)
1209 $(eval $(call gb_Library_add_exception_objects,sal,\\
1210         sal/osl/mac/mac \\
1212 else
1213 $(eval $(call gb_Library_add_exception_objects,sal,\\
1214         sal/osl/unx/uunxapi \\
1216 endif
1218         lines = source.split('\n')
1219         groups = {'':[]}
1220         groups = parse_makefile(groups, lines, 0, None, 0)
1221         self.assertEqual(len(groups), 3)
1222         self.assertEqual(len(groups['']), 0)
1223         self.assertEqual(len(groups['MACOSX']), 1)
1224         self.assertEqual(len(groups['!MACOSX']), 1)
1225         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1226         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1228     def test_parse_nested_if(self):
1229         source = """ifeq ($(OS),MACOSX)
1230 $(eval $(call gb_Library_add_exception_objects,sal,\\
1231         sal/osl/mac/mac \\
1233 else
1234 $(eval $(call gb_Library_add_exception_objects,sal,\\
1235         sal/osl/unx/uunxapi \\
1238 ifeq ($(OS),LINUX)
1239 $(eval $(call gb_Library_add_exception_objects,sal,\\
1240         sal/textenc/context \\
1242 endif
1243 endif
1245         lines = source.split('\n')
1246         groups = {'':[]}
1247         groups = parse_makefile(groups, lines, 0, None, 0)
1248         self.assertEqual(len(groups), 4)
1249         self.assertEqual(len(groups['']), 0)
1250         self.assertEqual(len(groups['MACOSX']), 1)
1251         self.assertEqual(len(groups['!MACOSX']), 1)
1252         self.assertEqual(len(groups['LINUX']), 1)
1253         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1254         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1255         self.assertEqual(groups['LINUX'][0], 'sal/textenc/context.cxx')
1257     def test_parse_exclude_system(self):
1258         source = """ifeq ($(OS),MACOSX)
1259 $(eval $(call gb_Library_add_exception_objects,sal,\\
1260         sal/osl/mac/mac \\
1262 else
1263 $(eval $(call gb_Library_add_exception_objects,sal,\\
1264         sal/osl/unx/uunxapi \\
1267 ifeq ($(OS),LINUX)
1268 $(eval $(call gb_Library_add_exception_objects,sal,\\
1269         sal/textenc/context \\
1271 endif
1272 endif
1274         global EXCLUDE_SYSTEM
1275         EXCLUDE_SYSTEM = True
1277         lines = source.split('\n')
1278         groups = {'':[]}
1279         groups = parse_makefile(groups, lines, 0, None, 0)
1280         self.assertEqual(len(groups), 1)
1281         self.assertEqual(len(groups['']), 0)
1283     def test_parse_filter(self):
1284         source = """ifneq ($(filter $(OS),MACOSX iOS),)
1285 $(eval $(call gb_Library_add_exception_objects,sal,\\
1286         sal/osl/unx/osxlocale \\
1288 endif
1290         # Filter is still unsupported.
1291         lines = source.split('\n')
1292         groups = {'':[]}
1293         groups = parse_makefile(groups, lines, 0, None, 0)
1294         self.assertEqual(len(groups), 1)
1295         self.assertEqual(len(groups['']), 0)
1297 unittest.main()
1299 # vim: set et sw=4 ts=4 expandtab: