update credits
[LibreOffice.git] / bin / update_pch
blob6112fc654555de7279d8ba39542d1f8c03c3d754
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
31 CUTOFF = 1
32 EXCLUDE_MODULE = False
33 EXCLUDE_LOCAL = False
34 EXCLUDE_SYSTEM = True
35 SILENT = False
36 WORKDIR = 'workdir'
38 # System includes: oox, sal, sd, svl, vcl
40 INCLUDE = False
41 EXCLUDE = True
42 DEFAULTS = \
44 #    module.library : (min, system, module, local), best time
45     'accessibility.acc'                 : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   7.8
46     'basctl.basctl'                     : ( 3, EXCLUDE, INCLUDE, EXCLUDE), #  11.9
47     'basegfx.basegfx'                   : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   3.8
48     'basic.sb'                          : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #  10.7
49     'chart2.chartcontroller'            : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  18.4
50     'chart2.chartcore'                  : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #  22.5
51     'comphelper.comphelper'             : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   7.6
52     'configmgr.configmgr'               : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   6.0
53     'connectivity.ado'                  : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   6.4
54     'connectivity.calc'                 : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
55     'connectivity.dbase'                : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   5.2
56     'connectivity.dbpool2'              : ( 5, EXCLUDE, INCLUDE, EXCLUDE), #   3.0
57     'connectivity.dbtools'              : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   0.8
58     'connectivity.file'                 : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   5.1
59     'connectivity.firebird_sdbc'        : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   5.1
60     'connectivity.flat'                 : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.6
61     'connectivity.mysql'                : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #   3.4
62     'connectivity.odbc'                 : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   5.0
63     'connectivity.postgresql-sdbc-impl' : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   6.7
64     'cppcanvas.cppcanvas'               : (11, EXCLUDE, INCLUDE, INCLUDE), #   4.8
65     'cppuhelper.cppuhelper'             : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
66     'cui.cui'                           : ( 8, EXCLUDE, INCLUDE, EXCLUDE), #  19.7
67     'dbaccess.dba'                      : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  13.8
68     'dbaccess.dbaxml'                   : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #   6.5
69     'dbaccess.dbu'                      : (12, EXCLUDE, EXCLUDE, EXCLUDE), #  23.6
70     'dbaccess.sdbt'                     : ( 1, EXCLUDE, INCLUDE, EXCLUDE), #   2.9
71     'desktop.deployment'                : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   6.1
72     'desktop.deploymentgui'             : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   5.7
73     'desktop.deploymentmisc'            : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #   3.4
74     'desktop.sofficeapp'                : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   6.5
75     'drawinglayer.drawinglayer'         : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   7.4
76     'editeng.editeng'                   : ( 5, EXCLUDE, INCLUDE, EXCLUDE), #  13.0
77     'forms.frm'                         : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #  14.2
78     'framework.fwe'                     : (10, EXCLUDE, INCLUDE, EXCLUDE), #   5.5
79     'framework.fwi'                     : ( 9, EXCLUDE, INCLUDE, EXCLUDE), #   3.4
80     'framework.fwk'                     : ( 7, EXCLUDE, INCLUDE, INCLUDE), #  14.8
81     'framework.fwl'                     : ( 5, EXCLUDE, INCLUDE, INCLUDE), #   5.1
82     'hwpfilter.hwp'                     : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   6.0
83     'lotuswordpro.lwpft'                : ( 2, EXCLUDE, EXCLUDE, EXCLUDE), #  11.6
84     'oox.oox'                           : ( 6, EXCLUDE, EXCLUDE, INCLUDE), #  28.2
85     'package.package2'                  : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   4.5
86     'package.xstor'                     : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   3.8
87     'reportdesign.rpt'                  : ( 9, EXCLUDE, INCLUDE, INCLUDE), #   9.4
88     'reportdesign.rptui'                : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  13.1
89     'reportdesign.rptxml'               : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   7.6
90     'sal.sal'                           : ( 2, EXCLUDE, EXCLUDE, INCLUDE), #   4.2
91     'sc.sc'                             : (12, EXCLUDE, INCLUDE, INCLUDE), #  92.6
92     'sc.scfilt'                         : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #  39.9
93     'sc.scui'                           : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #  15.0
94     'sc.vbaobj'                         : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #  17.3
95     'sd.sd'                             : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #  47.4
96     'sd.sdui'                           : ( 4, EXCLUDE, INCLUDE, INCLUDE), #   9.4
97     'sdext.PresentationMinimizer'       : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.1
98     'sdext.PresenterScreen'             : ( 2, EXCLUDE, INCLUDE, EXCLUDE), #   7.1
99     'sfx2.sfx'                          : ( 3, EXCLUDE, EXCLUDE, EXCLUDE), #  27.4
100     'slideshow.slideshow'               : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #  10.8
101     'sot.sot'                           : ( 5, EXCLUDE, EXCLUDE, INCLUDE), #   3.1
102     'starmath.sm'                       : ( 5, EXCLUDE, EXCLUDE, INCLUDE), #  10.9
103     'svgio.svgio'                       : ( 8, EXCLUDE, EXCLUDE, INCLUDE), #   4.3
104     'emfio.emfio'                       : ( 8, EXCLUDE, EXCLUDE, INCLUDE), #   4.3
105     'svl.svl'                           : ( 6, EXCLUDE, EXCLUDE, EXCLUDE), #   7.6
106     'svtools.svt'                       : ( 4, EXCLUDE, INCLUDE, EXCLUDE), #  17.6
107     'svx.svx'                           : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #  20.7
108     'svx.svxcore'                       : ( 7, EXCLUDE, INCLUDE, EXCLUDE), #  37.0
109     'sw.msword'                         : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  22.4
110     'sw.sw'                             : ( 7, EXCLUDE, EXCLUDE, INCLUDE), # 129.6
111     'sw.swui'                           : ( 3, EXCLUDE, INCLUDE, INCLUDE), #  26.1
112     'sw.vbaswobj'                       : ( 4, EXCLUDE, INCLUDE, INCLUDE), #  13.1
113     'tools.tl'                          : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #   4.2
114     'unotools.utl'                      : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
115     'unoxml.unoxml'                     : ( 1, EXCLUDE, EXCLUDE, EXCLUDE), #   4.6
116     'uui.uui'                           : ( 4, EXCLUDE, EXCLUDE, EXCLUDE), #   4.9
117     'vbahelper.msforms'                 : ( 3, EXCLUDE, INCLUDE, INCLUDE), #   5.2
118     'vbahelper.vbahelper'               : ( 3, EXCLUDE, EXCLUDE, INCLUDE), #   7.0
119     'vcl.vcl'                           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #  35.7
120     'writerfilter.writerfilter'         : ( 5, EXCLUDE, EXCLUDE, EXCLUDE), #  19.7/27.3
121     'xmloff.xo'                         : ( 7, EXCLUDE, INCLUDE, INCLUDE), #  22.1
122     'xmloff.xof'                        : ( 1, EXCLUDE, EXCLUDE, INCLUDE), #   4.4
123     'xmlscript.xmlscript'               : ( 4, EXCLUDE, EXCLUDE, INCLUDE), #   3.6
124     'xmlsecurity.xmlsecurity'           : ( 6, EXCLUDE, INCLUDE, INCLUDE), #   5.1
125     'xmlsecurity.xsec_xmlsec'           : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   4.4
126     'xmlsecurity.xsec_gpg'              : ( 2, EXCLUDE, INCLUDE, INCLUDE), #   ?
129 def remove_rare(raw, min_use=-1):
130     """ Remove headers not commonly included.
131         The minimum threshold is min_use.
132     """
133     # The minimum number of times a header
134     # must be included to be in the PCH.
135     min_use = min_use if min_use >= 0 else CUTOFF
137     out = []
138     if not raw or not len(raw):
139         return out
141     inc = sorted(raw)
142     last = inc[0]
143     count = 1
144     for x in range(1, len(inc)):
145         i = inc[x]
146         if i == last:
147             count += 1
148         else:
149             if count >= min_use:
150                 out.append(last)
151             last = i
152             count = 1
154     # Last group.
155     if count >= min_use:
156         out.append(last)
158     return out
160 def process_list(list, callable):
161     """ Given a list and callable
162         we pass each entry through
163         the callable and only add to
164         the output if not blank.
165     """
166     out = []
167     for i in list:
168         line = callable(i)
169         if line and len(line):
170             out.append(line)
171     return out
173 def find_files(path, recurse=True):
174     list = []
175     for root, dir, files in os.walk(path):
176         list += map(lambda x: os.path.join(root, x), files)
177     return list
179 def get_filename(line):
180     """ Strips the line from the
181         '#include' and angled brakets
182         and return the filename only.
183     """
184     if not len(line) or line[0] != '#':
185         return line
186     return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
188 def is_c_runtime(inc, root, module):
189     """ Heuristic-based detection of C/C++
190         runtime headers.
191         They are all-lowercase, with .h or
192         no extension, filename only.
193         Try to check that they are not LO headers.
194     """
195     inc = get_filename(inc)
197     if inc.endswith('.hxx') or inc.endswith('.hpp'):
198         return False
200     if inc.endswith('.h') and inc.startswith( 'config_' ):
201         return False
203     for c in inc:
204         if c == '/':
205             return False
206         if c == '.' and not inc.endswith('.h'):
207             return False
208         if c.isupper():
209             return False
211     if os.path.isfile( os.path.join(root, module, 'inc', inc)):
212         return False
214     return True
216 def sanitize(raw):
217     """ There are two forms of includes,
218         those with <> and "".
219         Technically, the difference is that
220         the compiler can use an internal
221         representation for an angled include,
222         such that it doesn't have to be a file.
223         For our purposes, there is no difference.
224         Here, we convert everything to angled.
225     """
226     if not raw or not len(raw):
227         return ''
228     raw = raw.strip()
229     if not len(raw):
230         return ''
231     return re.sub(r'(.*#include\s*)\"(.*)\"(.*)', r'#include <\2>', raw)
233 class Filter_Local(object):
234     """ Filter headers local to a module.
235         allow_public: allows include/module/file.hxx
236                       #include <module/file.hxx>
237         allow_module: allows module/inc/file.hxx
238                       #include <file.hxx>
239         allow_locals: allows module/source/file.hxx and
240                              module/source/inc/file.hxx
241                       #include <file.hxx>
242     """
243     def __init__(self, root, module, allow_public=True, allow_module=True, allow_locals=True):
244         self.root = root
245         self.module = module
246         self.allow_public = allow_public
247         self.allow_module = allow_module
248         self.allow_locals = allow_locals
249         self.public_prefix = '<' + self.module + '/'
251         all = find_files(os.path.join(root, module))
252         self.module_includes = []
253         self.locals = []
254         mod_prefix = module + '/inc/'
255         for i in all:
256             if mod_prefix in i:
257                 self.module_includes.append(i)
258             else:
259                 self.locals.append(i)
261     def is_public(self, line):
262         return self.public_prefix in line
264     def is_module(self, line):
265         """ Returns True if in module/inc/... """
266         filename = get_filename(line)
267         for i in self.module_includes:
268             if i.endswith(filename):
269                 return True
270         return False
272     def is_local(self, line):
273         """ Returns True if in module/source/... """
274         filename = get_filename(line)
275         for i in self.locals:
276             if i.endswith(filename):
277                 return True
278         return False
280     def is_external(self, line):
281         return is_c_runtime(line, self.root, self.module) and \
282                not self.is_public(line) and \
283                not self.is_module(line) and \
284                not self.is_local(line)
286     def find_local_file(self, line):
287         """ Finds the header file in the module dir,
288             but doesn't validate.
289         """
290         filename = get_filename(line)
291         for i in self.locals:
292             if i.endswith(filename):
293                 return i
294         for i in self.module_includes:
295             if i.endswith(filename):
296                 return i
297         return None
299     def proc(self, line):
300         assert line and len(line)
301         assert line[0] != '<' and line[0] != '#'
303         filename = get_filename(line)
305         # Local with relative path.
306         if filename.startswith('..'):
307             # Exclude for now as we don't have cxx path.
308             return ''
310         # Locals are included first (by the compiler).
311         if self.is_local(filename):
312             return line if self.allow_locals and '/inc/' in filename else ''
314         # Module headers are next.
315         if self.is_module(filename):
316             return line if self.allow_module else ''
318         # Public headers are last.
319         if self.is_public(line):
320             return line if self.allow_public else ''
322         # Leave out potentially unrelated files local
323         # to some other module we can't include directly.
324         if '/' not in filename and not self.is_external(filename):
325             return ''
327         # Unfiltered.
328         return line
330 def filter_ignore(line, module):
331     """ Filters includes from known
332         problematic ones.
333         Expects sanitized input.
334     """
335     assert line and len(line)
337     # Always include files without extension.
338     if '.' not in line:
339         return line
341     # Extract filenames for ease of comparison.
342     line = get_filename(line)
344     # Filter out all files that are not normal headers.
345     if not line.endswith('.h') and \
346        not line.endswith('.hxx') and \
347        not line.endswith('.hpp') and \
348        not line.endswith('.hdl'):
349        return ''
351     ignore_list = [
352             'LibreOfficeKit/LibreOfficeKitEnums.h', # Needs special directives
353             'LibreOfficeKit/LibreOfficeKitTypes.h', # Needs special directives
354             'jerror.h',     # c++ unfriendly
355             'jpeglib.h',    # c++ unfriendly
356             'boost/spirit/include/classic_core.hpp', # depends on BOOST_SPIRIT_DEBUG
357             'svtools/editimplementation.hxx' # no direct include
358         ]
360     if module == 'accessibility':
361         ignore_list += [
362             # STR_SVT_ACC_LISTENTRY_SELCTED_STATE redefined from svtools.hrc
363             'accessibility/extended/textwindowaccessibility.hxx',
364             ]
365     if module == 'basic':
366         ignore_list += [
367             'basic/vbahelper.hxx',
368             ]
369     if module == 'connectivity':
370         ignore_list += [
371             'com/sun/star/beans/PropertyAttribute.hpp', # OPTIONAL defined via objbase.h
372             'com/sun/star/sdbcx/Privilege.hpp', # DELETE defined via objbase.h
373             ]
374     if module == 'sc':
375         ignore_list += [
376             'progress.hxx', # special directives
377             'scslots.hxx',  # special directives
378            ]
379     if module == 'sd':
380         ignore_list += [
381             'sdgslots.hxx', # special directives
382             'sdslots.hxx',  # special directives
383            ]
384     if module == 'sfx2':
385         ignore_list += [
386             'sfx2/recentdocsview.hxx', # Redefines ApplicationType defined in objidl.h
387             'sfx2/sidebar/Sidebar.hxx',
388             'sfx2/sidebar/UnoSidebar.hxx',
389             'sfxslots.hxx', # externally defined types
390             ]
391     if module == 'sot':
392         ignore_list += [
393             'sysformats.hxx',   # Windows headers
394             ]
395     if module == 'vcl':
396         ignore_list += [
397             'accmgr.hxx',   # redefines ImplAccelList
398             'image.h',
399             'jobset.h',
400             'opengl/gdiimpl.hxx',
401             'opengl/salbmp.hxx',
402             'openglgdiimpl',   # ReplaceTextA
403             'printdlg.hxx',
404             'salinst.hxx',  # GetDefaultPrinterA
405             'salprn.hxx',   # SetPrinterDataA
406             'vcl/jobset.hxx',
407             'vcl/oldprintadaptor.hxx',
408             'vcl/opengl/OpenGLContext.hxx',
409             'vcl/opengl/OpenGLHelper.hxx',  # Conflicts with X header on *ix
410             'vcl/print.hxx',
411             'vcl/prntypes.hxx', # redefines Orientation from filter/jpeg/Exif.hxx
412             'vcl/sysdata.hxx',
413             ]
414     if module == 'xmloff':
415         ignore_list += [
416             'SchXMLExport.hxx', # SchXMLAutoStylePoolP.hxx not found
417             'SchXMLImport.hxx', # enums redefined in draw\sdxmlimp_impl.hxx
418             'XMLEventImportHelper.hxx', # NameMap redefined in XMLEventExport.hxx
419             'xmloff/XMLEventExport.hxx', # enums redefined
420             ]
421     if module == 'xmlsecurity':
422         ignore_list += [
423             'xmlsec/*',
424             'xmlsecurity/xmlsec-wrapper.h',
425             ]
426     if module == 'external/pdfium':
427         ignore_list += [
428             'third_party/freetype/include/pstables.h',
429             ]
430     if module == 'external/clucene':
431         ignore_list += [
432             '_bufferedstream.h',
433             '_condition.h',
434             '_gunichartables.h',
435             '_threads.h',
436             'error.h',
437             'CLucene/LuceneThreads.h',
438             'CLucene/config/_threads.h'
439             ]
441     for i in ignore_list:
442         if line.startswith(i):
443             return ''
444         if i[0] == '*' and line.endswith(i[1:]):
445             return ''
446         if i[-1] == '*' and line.startswith(i[:-1]):
447             return ''
449     return line
451 def fixup(includes, module):
452     """ Here we add any headers
453         necessary in the pch.
454         These could be known to be very
455         common but for technical reasons
456         left out of the pch by this generator.
457         Or, they could be missing from the
458         source files where they are used
459         (probably because they had been
460         in the old pch, they were missed).
461         Also, these could be headers
462         that make the build faster but
463         aren't added automatically.
464     """
465     fixes = []
466     def append(inc):
467         # Add a space to exclude from
468         # ignore bisecting.
469         line = ' #include <{}>'.format(inc)
470         try:
471             i = fixes.index(inc)
472             fixes[i] = inc
473         except:
474             fixes.append(inc)
476     if module == 'basctl':
477         if 'basslots.hxx' in includes:
478             append('sfx2/msg.hxx')
480     #if module == 'sc':
481     #    if 'scslots.hxx' in includes:
482     #        append('sfx2/msg.hxx')
483     return fixes
485 def sort_by_category(list, root, module, filter_local):
486     """ Move all 'system' headers first.
487         Core files of osl, rtl, sal, next.
488         Everything non-module-specific third.
489         Last, module-specific headers.
490     """
491     sys = []
492     boo = []
493     cor = []
494     rst = []
495     mod = []
497     prefix = '<' + module + '/'
498     for i in list:
499         if is_c_runtime(i, root, module):
500             sys.append(i)
501         elif '<boost/' in i:
502             boo.append(i)
503         elif prefix in i or not '/' in i:
504             mod.append(i)
505         elif '<sal/' in i or '<vcl/' in i:
506             cor.append(i)
507         elif '<osl/' in i or '<rtl/' in i:
508             if module == "sal": # osl and rtl are also part of sal
509                 mod.append(i)
510             else:
511                 cor.append(i)
512         # Headers from another module that is closely tied to the module.
513         elif module == 'sc' and '<formula' in i:
514             mod.append(i)
515         else:
516             rst.append(i)
518     out = []
519     out += [ "#if PCH_LEVEL >= 1" ]
520     out += sorted(sys)
521     out += sorted(boo)
522     out += [ "#endif // PCH_LEVEL >= 1" ]
523     out += [ "#if PCH_LEVEL >= 2" ]
524     out += sorted(cor)
525     out += [ "#endif // PCH_LEVEL >= 2" ]
526     out += [ "#if PCH_LEVEL >= 3" ]
527     out += sorted(rst)
528     out += [ "#endif // PCH_LEVEL >= 3" ]
529     out += [ "#if PCH_LEVEL >= 4" ]
530     out += sorted(mod)
531     out += [ "#endif // PCH_LEVEL >= 4" ]
532     return out
534 def parse_makefile(groups, lines, lineno, lastif, ifstack):
536     inobjects = False
537     ingeneratedobjects = False
538     inelse = False
539     suffix = 'cxx'
540     os_cond_re = re.compile('(ifeq|ifneq)\s*\(\$\(OS\)\,(\w*)\)')
542     line = lines[lineno]
543     if line.startswith('if'):
544         lastif = line
545         if ifstack == 0:
546             # Correction if first line is an if.
547             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
548     else:
549         lineno -= 1
551     while lineno + 1 < len(lines):
552         lineno += 1
553         line = lines[lineno].strip()
554         line = line.rstrip('\\').strip()
555         #print('line #{}: {}'.format(lineno, line))
556         if len(line) == 0:
557             continue
559         if line == '))':
560             inobjects = False
561             ingeneratedobjects = False
562         elif 'add_exception_objects' in line or \
563              'add_cxxobject' in line:
564              inobjects = True
565              #print('inobjects')
566              #if ifstack and not SILENT:
567                 #sys.stderr.write('Sources in a conditional, ignoring for now.\n')
568         elif 'add_generated_exception_objects' in line or \
569              'add_generated_cxxobject' in line:
570              ingeneratedobjects = True
571         elif 'set_generated_cxx_suffix' in line:
572             suffix_re = re.compile('.*set_generated_cxx_suffix,[^,]*,([^)]*).*')
573             match = suffix_re.match(line)
574             if match:
575                 suffix = match.group(1)
576         elif line.startswith('if'):
577             lineno = parse_makefile(groups, lines, lineno, line, ifstack+1)
578             continue
579         elif line.startswith('endif'):
580             if ifstack:
581                 return lineno
582             continue
583         elif line.startswith('else'):
584             inelse = True
585         elif inobjects or ingeneratedobjects:
586             if EXCLUDE_SYSTEM and ifstack:
587                 continue
588             file = line + '.' + suffix
589             if ',' in line or '(' in line or ')' in line:
590                 #print('passing: ' + line)
591                 pass # $if() probably, or something similar
592             else:
593                 osname = ''
594                 if lastif:
595                     if 'filter' in lastif:
596                         # We can't grok filter, yet.
597                         continue
598                     match = os_cond_re.match(lastif)
599                     if not match:
600                         # We only support OS conditionals.
601                         continue
602                     in_out = match.group(1)
603                     osname = match.group(2) if match else ''
604                     if (in_out == 'ifneq' and not inelse) or \
605                        (in_out == 'ifeq' and inelse):
606                         osname = '!' + osname
608                 if osname not in groups:
609                     groups[osname] = []
610                 if ingeneratedobjects:
611                     file = WORKDIR + '/' + file
612                 groups[osname].append(file)
614     return groups
616 def process_makefile(root, module, libname):
617     """ Parse a gmake makefile and extract
618         source filenames from it.
619     """
621     makefile = 'Library_{}.mk'.format(libname)
622     filename = os.path.join(os.path.join(root, module), makefile)
623     if not os.path.isfile(filename):
624         makefile = 'StaticLibrary_{}.mk'.format(libname)
625         filename = os.path.join(os.path.join(root, module), makefile)
626         if not os.path.isfile(filename):
627             sys.stderr.write('Error: Module {} has no makefile at {}.'.format(module, filename))
629     groups = {'':[], 'ANDROID':[], 'iOS':[], 'WNT':[], 'LINUX':[], 'MACOSX':[]}
631     with open(filename, 'r') as f:
632         lines = f.readlines()
633         groups = parse_makefile(groups, lines, lineno=0, lastif=None, ifstack=0)
635     return groups
637 def is_allowed_if(line, module):
638     """ Check whether the given #if condition
639         is allowed for the given module or whether
640         its block should be ignored.
641     """
643     # remove trailing comments
644     line = re.sub(r'(.*) *//.*', r'\1', line)
645     line = line.strip()
647     # Our sources always build with LIBO_INTERNAL_ONLY.
648     if line == "#if defined LIBO_INTERNAL_ONLY" or line == "#ifdef LIBO_INTERNAL_ONLY":
649         return True
650     if module == "external/skia":
651         # We always set these.
652         if line == "#ifdef SK_VULKAN" or line == "#if SK_SUPPORT_GPU":
653             return True
654     return False
656 def process_source(root, module, filename, maxdepth=0):
657     """ Process a source file to extract
658         included headers.
659         For now, skip on compiler directives.
660         maxdepth is used when processing headers
661         which typically have protecting ifndef.
662     """
664     ifdepth = 0
665     lastif = ''
666     raw_includes = []
667     allowed_ifs = []
668     ifsallowed = 0
669     with open(filename, 'r') as f:
670         for line in f:
671             line = line.strip()
672             if line.startswith('#if'):
673                 if is_allowed_if(line, module):
674                     allowed_ifs.append(True)
675                     ifsallowed += 1
676                 else:
677                     allowed_ifs.append(False)
678                     lastif = line
679                 ifdepth += 1
680             elif line.startswith('#endif'):
681                 ifdepth -= 1
682                 if allowed_ifs[ ifdepth ]:
683                     ifsallowed -= 1
684                 else:
685                     lastif = '#if'
686                 del allowed_ifs[ ifdepth ]
687             elif line.startswith('#include'):
688                 if ifdepth - ifsallowed <= maxdepth:
689                     line = sanitize(line)
690                     if line:
691                         line = get_filename(line)
692                     if line and len(line):
693                         raw_includes.append(line)
694                 elif not SILENT:
695                     sys.stderr.write('#include in {} : {}\n'.format(lastif, line))
697     return raw_includes
699 def explode(root, module, includes, tree, filter_local, recurse):
700     incpath = os.path.join(root, 'include')
702     for inc in includes:
703         filename = get_filename(inc)
704         if filename in tree or len(filter_local.proc(filename)) == 0:
705             continue
707         try:
708             # Module or Local header.
709             filepath = filter_local.find_local_file(inc)
710             if filepath:
711                 #print('trying loc: ' + filepath)
712                 incs = process_source(root, module, filepath, maxdepth=1)
713                 incs = map(get_filename, incs)
714                 incs = process_list(incs, lambda x: filter_ignore(x, module))
715                 incs = process_list(incs, filter_local.proc)
716                 tree[filename] = incs
717                 if recurse:
718                     tree = explode(root, module, incs, tree, filter_local, recurse)
719                 #print('{} => {}'.format(filepath, tree[filename]))
720                 continue
721         except:
722             pass
724         try:
725             # Public header.
726             filepath = os.path.join(incpath, filename)
727             #print('trying pub: ' + filepath)
728             incs = process_source(root, module, filepath, maxdepth=1)
729             incs = map(get_filename, incs)
730             incs = process_list(incs, lambda x: filter_ignore(x, module))
731             incs = process_list(incs, filter_local.proc)
732             tree[filename] = incs
733             if recurse:
734                 tree = explode(root, module, incs, tree, filter_local, recurse)
735             #print('{} => {}'.format(filepath, tree[filename]))
736             continue
737         except:
738             pass
740         # Failed, but remember to avoid searching again.
741         tree[filename] = []
743     return tree
745 def make_command_line():
746     args = sys.argv[:]
747     # Remove command line flags and
748     # use internal flags.
749     for i in range(len(args)-1, 0, -1):
750         if args[i].startswith('--'):
751             args.pop(i)
753     args.append('--cutoff=' + str(CUTOFF))
754     if EXCLUDE_SYSTEM:
755         args.append('--exclude:system')
756     else:
757         args.append('--include:system')
758     if EXCLUDE_MODULE:
759         args.append('--exclude:module')
760     else:
761         args.append('--include:module')
762     if EXCLUDE_LOCAL:
763         args.append('--exclude:local')
764     else:
765         args.append('--include:local')
767     return ' '.join(args)
769 def generate_includes(includes):
770     """Generates the include lines of the pch.
771     """
772     lines = []
773     for osname, group in includes.items():
774         if not len(group):
775             continue
777         if len(osname):
778             not_eq = ''
779             if osname[0] == '!':
780                 not_eq = '!'
781                 osname = osname[1:]
782             lines.append('')
783             lines.append('#if {}defined({})'.format(not_eq, osname))
785         for i in group:
786             lines.append(i)
788         if len(osname):
789             lines.append('#endif')
791     return lines
793 def generate(includes, libname, filename, module):
794     header = \
795 """/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
797  * This file is part of the LibreOffice project.
799  * This Source Code Form is subject to the terms of the Mozilla Public
800  * License, v. 2.0. If a copy of the MPL was not distributed with this
801  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
802  */
805  This file has been autogenerated by update_pch.sh. It is possible to edit it
806  manually (such as when an include file has been moved/renamed/removed). All such
807  manual changes will be rewritten by the next run of update_pch.sh (which presumably
808  also fixes all possible problems, so it's usually better to use it).
811     footer = \
813 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
815     import datetime
817     with open(filename, 'w') as f:
818         f.write(header)
819         f.write('\n Generated on {} using:\n {}\n'.format(
820                 datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
821                 make_command_line()))
822         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(
823                 filename, module))
825         # sal needs this for rand_s()
826         if module == 'sal' and libname == 'sal':
827             sal_define = """
828 #if defined(_WIN32)
829 #if !defined _CRT_RAND_S
830 #define _CRT_RAND_S
831 #endif
832 #endif
834             f.write(sal_define)
836         # Dump the headers.
837         f.write('\n')
838         for i in includes:
839             f.write(i + '\n')
841         # Some libraries pull windows headers that aren't self contained.
842         if (module == 'connectivity' and libname == 'ado') or \
843            (module == 'xmlsecurity' and libname == 'xsec_xmlsec'):
844             ado_define = """
845 // Cleanup windows header macro pollution.
846 #if defined(_WIN32) && defined(WINAPI)
847 #   include <postwin.h>
848 #   undef RGB
849 #endif
851             f.write(ado_define)
853         f.write(footer)
855 def remove_from_tree(filename, tree):
856     # Remove this file, if top-level.
857     incs = tree.pop(filename, [])
858     for i in incs:
859         tree = remove_from_tree(i, tree)
861     # Also remove if included from another.
862     for (k, v) in tree.items():
863         if filename in v:
864             v.remove(filename)
866     return tree
868 def tree_to_list(includes, filename, tree):
869     if filename in includes:
870         return includes
871     includes.append(filename)
872     #incs = tree.pop(filename, [])
873     incs = tree[filename] if filename in tree else []
874     for i in incs:
875         tree_to_list(includes, i, tree)
877     return includes
879 def promote(includes):
880     """ Common library headers are heavily
881         referenced, even if they are included
882         from a few places.
883         Here we separate them to promote
884         their inclusion in the final pch.
885     """
886     promo = []
887     for inc in includes:
888         if inc.startswith('boost') or \
889            inc.startswith('sal') or \
890            inc.startswith('osl') or \
891            inc.startswith('rtl'):
892             promo.append(inc)
893     return promo
895 def make_pch_filename(root, module, libname):
896     """ PCH files are stored here:
897         <root>/<module>/inc/pch/precompiled_<libname>.hxx
898     """
900     path = os.path.join(root, module)
901     path = os.path.join(path, 'inc')
902     path = os.path.join(path, 'pch')
903     path = os.path.join(path, 'precompiled_' + libname + '.hxx')
904     return path
906 def main():
908     global CUTOFF
909     global EXCLUDE_MODULE
910     global EXCLUDE_LOCAL
911     global EXCLUDE_SYSTEM
912     global SILENT
913     global WORKDIR
915     if os.getenv('WORKDIR'):
916         WORKDIR = os.getenv('WORKDIR')
918     root = '.'
919     module = sys.argv[1]
920     libname = sys.argv[2]
921     header = make_pch_filename(root, module, libname)
923     if not os.path.exists(os.path.join(root, module)):
924         raise Exception('Error: module [{}] not found.'.format(module))
926     key = '{}.{}'.format(module, libname)
927     if key in DEFAULTS:
928         # Load the module-specific defaults.
929         CUTOFF = DEFAULTS[key][0]
930         EXCLUDE_SYSTEM = DEFAULTS[key][1]
931         EXCLUDE_MODULE = DEFAULTS[key][2]
932         EXCLUDE_LOCAL = DEFAULTS[key][3]
934     force_update = False
935     for x in range(3, len(sys.argv)):
936         i = sys.argv[x]
937         if i.startswith('--cutoff='):
938             CUTOFF = int(i.split('=')[1])
939         elif i.startswith('--exclude:'):
940             cat = i.split(':')[1]
941             if cat == 'module':
942                 EXCLUDE_MODULE = True
943             elif cat == 'local':
944                 EXCLUDE_LOCAL = True
945             elif cat == 'system':
946                 EXCLUDE_SYSTEM = True
947         elif i.startswith('--include:'):
948             cat = i.split(':')[1]
949             if cat == 'module':
950                 EXCLUDE_MODULE = False
951             elif cat == 'local':
952                 EXCLUDE_LOCAL = False
953             elif cat == 'system':
954                 EXCLUDE_SYSTEM = False
955         elif i == '--silent':
956             SILENT = True
957         elif i == '--force':
958             force_update = True
959         else:
960             sys.stderr.write('Unknown option [{}].'.format(i))
961             return 1
963     filter_local = Filter_Local(root, module, \
964                                 not EXCLUDE_MODULE, \
965                                 not EXCLUDE_LOCAL)
967     # Read input.
968     groups = process_makefile(root, module, libname)
970     generic = []
971     for osname, group in groups.items():
972         if not len(group):
973             continue
975         includes = []
976         for filename in group:
977             includes += process_source(root, module, filename)
979         # Save unique top-level includes.
980         unique = set(includes)
981         promoted = promote(unique)
983         # Process includes.
984         includes = remove_rare(includes)
985         includes = process_list(includes, lambda x: filter_ignore(x, module))
986         includes = process_list(includes, filter_local.proc)
988         # Remove the already included ones.
989         for inc in includes:
990             unique.discard(inc)
992         # Explode the excluded ones.
993         tree = {i:[] for i in includes}
994         tree = explode(root, module, unique, tree, filter_local, not EXCLUDE_MODULE)
996         # Remove the already included ones from the tree.
997         for inc in includes:
998             filename = get_filename(inc)
999             tree = remove_from_tree(filename, tree)
1001         extra = []
1002         for (k, v) in tree.items():
1003             extra += tree_to_list([], k, tree)
1005         promoted += promote(extra)
1006         promoted = process_list(promoted, lambda x: filter_ignore(x, module))
1007         promoted = process_list(promoted, filter_local.proc)
1008         promoted = set(promoted)
1009         # If a promoted header includes others, remove the rest.
1010         for (k, v) in tree.items():
1011             if k in promoted:
1012                 for i in v:
1013                     promoted.discard(i)
1014         includes += [x for x in promoted]
1016         extra = remove_rare(extra)
1017         extra = process_list(extra, lambda x: filter_ignore(x, module))
1018         extra = process_list(extra, filter_local.proc)
1019         includes += extra
1021         includes = [x for x in set(includes)]
1022         fixes = fixup(includes, module)
1023         fixes = map(lambda x: '#include <' + x + '>', fixes)
1025         includes = map(lambda x: '#include <' + x + '>', includes)
1026         sorted = sort_by_category(includes, root, module, filter_local)
1027         includes = list(fixes) + sorted
1029         if len(osname):
1030             for i in generic:
1031                 if i in includes:
1032                     includes.remove(i)
1034         groups[osname] = includes
1035         if not len(osname):
1036             generic = includes
1038     # Open the old pch and compare its contents
1039     # with new includes.
1040     # Clobber only if they are different.
1041     with open(header, 'r') as f:
1042         old_pch_lines = [x.strip() for x in f.readlines()]
1043         new_lines = generate_includes(groups)
1044         # Find the first include in the old pch.
1045         start = -1
1046         for i in range(len(old_pch_lines)):
1047             if old_pch_lines[i].startswith('#include') or old_pch_lines[i].startswith('#if PCH_LEVEL'):
1048                 start = i
1049                 break
1050         # Clobber if there is a mismatch.
1051         if force_update or start < 0 or (len(old_pch_lines) - start < len(new_lines)):
1052             generate(new_lines, libname, header, module)
1053             return 0
1054         else:
1055             for i in range(len(new_lines)):
1056                 if new_lines[i] != old_pch_lines[start + i]:
1057                     generate(new_lines, libname, header, module)
1058                     return 0
1059             else:
1060                 # Identical, but see if new pch removed anything.
1061                 for i in range(start + len(new_lines), len(old_pch_lines)):
1062                     if '#include' in old_pch_lines[i]:
1063                         generate(new_lines, libname, header, module)
1064                         return 0
1066     # Didn't update.
1067     return 1
1069 if __name__ == '__main__':
1070     """ Process all the includes in a Module
1071         to make into a PCH file.
1072         Run without arguments for unittests,
1073         and to see usage.
1074     """
1076     if len(sys.argv) >= 3:
1077         status = main()
1078         sys.exit(status)
1080     print('Usage: {} <Module name> <Library name> [options]'.format(sys.argv[0]))
1081     print('    Always run from the root of LO repository.\n')
1082     print('    Options:')
1083     print('    --cutoff=<count> - Threshold to excluding headers.')
1084     print('    --exclude:<category> - Exclude category-specific headers.')
1085     print('    --include:<category> - Include category-specific headers.')
1086     print('    --force - Force updating the pch even when nothing changes.')
1087     print('    Categories:')
1088     print('         module - Headers in /inc directory of a module.')
1089     print('         local  - Headers local to a source file.')
1090     print('         system - Platform-specific headers.')
1091     print('    --silent - print only errors.')
1092     print('\nRunning unit-tests...')
1095 class TestMethods(unittest.TestCase):
1097     def test_sanitize(self):
1098         self.assertEqual(sanitize('#include "blah/file.cxx"'),
1099                                 '#include <blah/file.cxx>')
1100         self.assertEqual(sanitize('  #include\t"blah/file.cxx" '),
1101                                 '#include <blah/file.cxx>')
1102         self.assertEqual(sanitize('  '),
1103                                 '')
1105     def test_filter_ignore(self):
1106         self.assertEqual(filter_ignore('blah/file.cxx', 'mod'),
1107                                      '')
1108         self.assertEqual(filter_ignore('vector', 'mod'),
1109                                      'vector')
1110         self.assertEqual(filter_ignore('file.cxx', 'mod'),
1111                                      '')
1113     def test_remove_rare(self):
1114         self.assertEqual(remove_rare([]),
1115                                 [])
1117 class TestMakefileParser(unittest.TestCase):
1119     def setUp(self):
1120         global EXCLUDE_SYSTEM
1121         EXCLUDE_SYSTEM = False
1123     def test_parse_singleline_eval(self):
1124         source = "$(eval $(call gb_Library_Library,sal))"
1125         lines = source.split('\n')
1126         groups = {'':[]}
1127         groups = parse_makefile(groups, lines, 0, None, 0)
1128         self.assertEqual(len(groups), 1)
1129         self.assertEqual(len(groups['']), 0)
1131     def test_parse_multiline_eval(self):
1132         source = """$(eval $(call gb_Library_set_include,sal,\\
1133         $$(INCLUDE) \\
1134         -I$(SRCDIR)/sal/inc \\
1137         lines = source.split('\n')
1138         groups = {'':[]}
1139         groups = parse_makefile(groups, lines, 0, None, 0)
1140         self.assertEqual(len(groups), 1)
1141         self.assertEqual(len(groups['']), 0)
1143     def test_parse_multiline_eval_with_if(self):
1144         source = """$(eval $(call gb_Library_add_defs,sal,\\
1145         $(if $(filter $(OS),iOS), \\
1146                 -DNO_CHILD_PROCESSES \\
1147         ) \\
1150         lines = source.split('\n')
1151         groups = {'':[]}
1152         groups = parse_makefile(groups, lines, 0, None, 0)
1153         self.assertEqual(len(groups), 1)
1154         self.assertEqual(len(groups['']), 0)
1156     def test_parse_multiline_add_with_if(self):
1157         source = """$(eval $(call gb_Library_add_exception_objects,sal,\\
1158         sal/osl/unx/time \\
1159         $(if $(filter DESKTOP,$(BUILD_TYPE)), sal/osl/unx/salinit) \\
1162         lines = source.split('\n')
1163         groups = {'':[]}
1164         groups = parse_makefile(groups, lines, 0, None, 0)
1165         self.assertEqual(len(groups), 1)
1166         self.assertEqual(len(groups['']), 1)
1167         self.assertEqual(groups[''][0], 'sal/osl/unx/time.cxx')
1169     def test_parse_if_else(self):
1170         source = """ifeq ($(OS),MACOSX)
1171 $(eval $(call gb_Library_add_exception_objects,sal,\\
1172         sal/osl/mac/mac \\
1174 else
1175 $(eval $(call gb_Library_add_exception_objects,sal,\\
1176         sal/osl/unx/uunxapi \\
1178 endif
1180         lines = source.split('\n')
1181         groups = {'':[]}
1182         groups = parse_makefile(groups, lines, 0, None, 0)
1183         self.assertEqual(len(groups), 3)
1184         self.assertEqual(len(groups['']), 0)
1185         self.assertEqual(len(groups['MACOSX']), 1)
1186         self.assertEqual(len(groups['!MACOSX']), 1)
1187         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1188         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1190     def test_parse_nested_if(self):
1191         source = """ifeq ($(OS),MACOSX)
1192 $(eval $(call gb_Library_add_exception_objects,sal,\\
1193         sal/osl/mac/mac \\
1195 else
1196 $(eval $(call gb_Library_add_exception_objects,sal,\\
1197         sal/osl/unx/uunxapi \\
1200 ifeq ($(OS),LINUX)
1201 $(eval $(call gb_Library_add_exception_objects,sal,\\
1202         sal/textenc/context \\
1204 endif
1205 endif
1207         lines = source.split('\n')
1208         groups = {'':[]}
1209         groups = parse_makefile(groups, lines, 0, None, 0)
1210         self.assertEqual(len(groups), 4)
1211         self.assertEqual(len(groups['']), 0)
1212         self.assertEqual(len(groups['MACOSX']), 1)
1213         self.assertEqual(len(groups['!MACOSX']), 1)
1214         self.assertEqual(len(groups['LINUX']), 1)
1215         self.assertEqual(groups['MACOSX'][0], 'sal/osl/mac/mac.cxx')
1216         self.assertEqual(groups['!MACOSX'][0], 'sal/osl/unx/uunxapi.cxx')
1217         self.assertEqual(groups['LINUX'][0], 'sal/textenc/context.cxx')
1219     def test_parse_exclude_system(self):
1220         source = """ifeq ($(OS),MACOSX)
1221 $(eval $(call gb_Library_add_exception_objects,sal,\\
1222         sal/osl/mac/mac \\
1224 else
1225 $(eval $(call gb_Library_add_exception_objects,sal,\\
1226         sal/osl/unx/uunxapi \\
1229 ifeq ($(OS),LINUX)
1230 $(eval $(call gb_Library_add_exception_objects,sal,\\
1231         sal/textenc/context \\
1233 endif
1234 endif
1236         global EXCLUDE_SYSTEM
1237         EXCLUDE_SYSTEM = True
1239         lines = source.split('\n')
1240         groups = {'':[]}
1241         groups = parse_makefile(groups, lines, 0, None, 0)
1242         self.assertEqual(len(groups), 1)
1243         self.assertEqual(len(groups['']), 0)
1245     def test_parse_filter(self):
1246         source = """ifneq ($(filter $(OS),MACOSX iOS),)
1247 $(eval $(call gb_Library_add_exception_objects,sal,\\
1248         sal/osl/unx/osxlocale \\
1250 endif
1252         # Filter is still unsupported.
1253         lines = source.split('\n')
1254         groups = {'':[]}
1255         groups = parse_makefile(groups, lines, 0, None, 0)
1256         self.assertEqual(len(groups), 1)
1257         self.assertEqual(len(groups['']), 0)
1259 unittest.main()
1261 # vim: set et sw=4 ts=4 expandtab: