File: pch.rb

package info (click to toggle)
mkvtoolnix 92.0-1
  • links: PTS
  • area: main
  • in suites: trixie
  • size: 58,620 kB
  • sloc: cpp: 216,810; ruby: 11,403; xml: 8,058; ansic: 6,885; sh: 4,884; python: 1,041; perl: 191; makefile: 113; awk: 16; javascript: 4
file content (659 lines) | stat: -rw-r--r-- 18,940 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
#
#   mkvtoolnix - programs for manipulating Matroska files
#   Copyright © 2003…2016 Moritz Bunkus
#
#   This file is distributed as part of mkvtoolnix and is distributed under the
#   GNU General Public License Version 2, or (at your option) any later version.
#   See accompanying file COPYING for details or visit
#   http://www.gnu.org/licenses/gpl.html .
#
#   precompiled header support
#
#   Authors: KonaBlend <kona8lend@gmail.com>
#

module PCH
  extend Rake::DSL

  #############################################################################
  #
  # Parser for customized variable number of task[options...] in white-space
  # separated, name=value pairs, with quoted strings permitted.
  #
  #   name       --> toggle action (must be a boolean)
  #   name=      --> set to default value
  #   name=value --> set to value
  #
  class Options
    def initialize(t, spec)
      @task = t
      @spec = spec
      @pairs = {}
    end

    def handler(key, &block)
      key = key.to_s
      if block.arity == 0 # reader has 0 args
        define_singleton_method(key.to_sym) { block.call }
      else
        @pairs.store(key, block)
      end
    end

    def parse
      return unless @spec
      parse_root do |k,v|
        fail "unknown name '#{k}' specified for task '#{@task.name}'" unless @pairs.has_key?(k)
        @pairs.fetch(k).call(k, v)
      end
    end

    def parse_root
      @index = 0
      @name = ''
      @value = nil

      while @index < @spec.length
        c = @spec[@index]
        if c == " " || c == "\t"
          @index += 1
        else
          parse_name
          yield @name,@value
          @name = ''
          @value = nil
        end
      end
    end

    def parse_name
      while @index < @spec.length
        c = @spec[@index]
        case
        when c == " "
          @index += 1
          break
        when c == "="
          fail "unexpected '#{c}' while parsing name" if @name.empty?
          @index += 1
          parse_value
          break
        else
          @name += c
          @index += 1
        end
      end
    end

    def parse_value
      @value = ''
      while @index < @spec.length
        c = @spec[@index]
        case
        when c == " "
          @index += 1
          break
        when c == '"' && (@index+1) < @spec.length
          @index += 1
          parse_quote
        else
          @value += c
          @index += 1
        end
      end
    end

    def parse_quote
      open = true
      while @index < @spec.length
        c = @spec[@index]
        case
        when c == '"'
          open = false
          @index += 1
          break
        when c == '\\' && (@index+1) < @spec.length
          @value += @spec[@index+1]
          @index += 2
        else
          @value += c
          @index += 1
        end
      end
      fail "unterminated quote while parsing value" if open
    end
  end

  #############################################################################

  def self.engage(&cxx_compiler)
    return unless c?(:USE_PRECOMPILED_HEADERS)
    load_config
    @users.clear
    $all_sources.each { |source| @users.store(source, nil) }
    @moc_users.each_key { |moc| @users.store(moc, Pathname.new(moc).sub_ext(".h").to_s) }
    namespace :pch do
      t = file(@config_file.to_s => @config_deps) { scan_users }
      t.invoke
    end
    add_tasks(&cxx_compiler)
    add_prerequisites
  end

  #############################################################################

  def self.add_tasks(&compiler)
    pchs = []
    headers = {}
    @db_scan.each_value { |h| headers.store(h, nil) }
    headers.keys.each do |header|
      pch = "#{header}#{@extension}"
      pchs.push(file pch => "#{header}", &compiler)
    end

    desc "Set pch related options (persistent)"
    task :pch, :options do |t,args|
      o = Options.new(t, args.options)

      o.handler(:htrace) { @htrace }
      o.handler(:htrace) do |k,v|
        case v
        when nil
          @htrace = !@htrace
        when ''
          @htrace = false
        when /^(true|yes|1)$/i
          @htrace = true
        when /^(false|no|0)$/i
          @htrace = false
        else
          fail "invalid value for pch[htrace]: #{v}"
        end
        puts "pch[htrace] = #{@htrace}"
      end

      o.handler(:pretty) { @pretty }
      o.handler(:pretty) do |k,v|
        case v
        when nil
          @pretty = !@pretty
        when ''
          @pretty = false
        when /^(true|yes|1)$/i
          @pretty = true
        when /^(false|no|0)$/i
          @pretty = false
        else
          fail "invalid value for pch[pretty]: #{v}"
        end
        puts "pch[pretty] = #{@pretty}"
      end

      o.parse
    end

    namespace :pch do
      desc "Overview"
      rself = Pathname.new(__FILE__)
      rself = rself.relative_path_from(Pathname.new(Dir.pwd)) unless rself.relative?
      task :overview do
        text = <<-ENDTEXT
:
:   PCH - precompiled header support
:
:   Precompiled headers are active when the host operating system and tools
:   are capable (as detected and enabled by configure). A PCH configuration
:   is saved in '#{@config_file.to_s}' and is automatically managed and
:   also participates with task 'clean:dist'.
:
:   The config is a task resolved at rake load-time. That is to say, this
:   task effectively behaves like a prerequisite to all other tasks.
:
:   PCH system will bootstrap by scanning all known user files (.cpp, .moc)
:   for idiomatic precompiled header usage. The pattern scanned for is:
:
:       #{@scan_include_re.to_s}
:
:   PCH will re-bootstrap if '#{@config_file.to_s} is out of date to any of:
:
<% @config_deps.each do |x| %>
:       <%= x %>
<% end %>
:
:   During source file compilation, logic has been added to probe give PCH
:   opportunity to supply additional compiler flags (-include) and thus use
:   a precompiled header.
:
:   The pch binary is added as a prerequisite for all of its users, so
:   pch generation is automatic, but tasks 'pch:all' and 'pch:clean' may be
:   used manually.
:
:   'pch:status' generates a short report on PCH state of affairs. Verbosity
:   may be increased like so; note shell quotes placed around target to
:   escape shell-special treatment of square-brackets.
:
:       #{Rake.application.name} "pch:status[verbose]"
:
:   'pch[htrace]' is persistent flag which (when true) causes all subsequent
:   source file compilation to record which header files were opened. The
:   output is saved to .htrace files and .htrace files are clean as part
:   of task 'clean'.
:
:   'pch[pretty]' is persistent flag which (when true) adds more information
:   about files as they are compiled.
:
        ENDTEXT
        ERB.new(text, nil, trim_mode="<>").run(binding)
      end

      desc "Generate all precompiled headers"
      task :all => pchs

      desc "Remove all precompiled headers"
      task :clean do
        pats = []
        pchs.each { |x| pats << x.name << (x.name + "-????????") }
        remove_files_by_patterns(pats)
      end

      desc "Scan candidate user files for precompiled header pattern."
      task :scan do
        scan_users
      end

      desc "Status of database"
      task :status, :options do |t,args|
        o = Options.new(t, args.options)

        o.handler(:verbose) { o.instance_exec { @verbose }}
        o.handler(:verbose) do |k,v|
          case v
          when nil
            o.instance_exec { @verbose = !@verbose }
          when ""
            o.instance_exec { @verbose = false }
          when /^(true|yes|1)$/i
            o.instance_exec { @verbose = true }
          when /^(false|no|0)$/i
            o.instance_exec { @verbose = false }
          else
            fail "invalid value for pch[verbose]: #{v}"
          end
        end
        o.parse
        status(o)
      end
    end

    Rake::Task['clean:dist'].enhance { @endblock_skip_save = true }
  end

  #############################################################################

  def self.add_prerequisites
    @db_scan.each_pair do |user, header|
      case File.extname(user)
      when '.moc'
        object = user + 'o'
        file object => user
      else
        object = Pathname.new(user).sub_ext('.o')
      end
      file object => "#{header}#{@extension}"
    end
  end

  #############################################################################

  # return hash of file types (extension) => count
  def self.file_types(files)
    r = {}
    files.each do |name|
      key = File.extname(name)
      i = r.fetch(key, 0)
      r.store(key, i+1)
    end
    r.sort_by { |k,v| k }.collect { |x| "#{x[0]}=#{x[1]}" }.join(", ")
  end

  def self.status(options)
    headers = {}
    @db_scan.each_value do |h|
       i = headers.fetch(h, 0)
       headers.store(h, i+1)
    end
    unmarked = @users.keys.clone
    unmarked.delete_if { |k,v| @db_scan.has_key?(k) }

    text = <<-ENDTEXT
PCH status: <%= c?(:USE_PRECOMPILED_HEADERS) ? "enabled" : "disabled" %>
:
:   htrace = #{@htrace}
:   pretty = #{@pretty}
:
: <%= "%8d %-40s (%s)" % [@users.size, "total users", file_types(@users.keys)] %>
: <%= "%8d %-40s (%s)" % [headers.size, "unique pch headers", file_types(headers.keys)] %>
: <%= "%8d %-40s (%s)" % [@db_scan.size, "users marked for pch use", file_types(@db_scan.keys)] %>
: <%= "%8d %-40s (%s)" % [unmarked.size, "users not marked", file_types(unmarked)] %>
<% headers.each_pair do |k,v| %>
: <%= "%8d %-40s (%s)" % [v,"users of %s" % [k], file_types(@db_scan.select { |k1,v1| v1 == k }.keys)] %>
<% end %>
    ENDTEXT

    text.concat( <<-ENDTEXT
<% lines = @db_scan.each_pair.collect { |k,v| ":   marked %s -> %s" % [k,v] } %>
<% unless lines.empty? %>

<% end %>
<%= lines.join("\n") %>
<% lines = unmarked.collect { |u| ": unmarked %s" % [u] } %>
<% unless lines.empty? %>

<% end %>
<%= lines.join("\n") %>
    ENDTEXT
  ) if options.verbose

    text << ":\n"
    ERB.new(text, nil, trim_mode="<>").run(binding)
  end

  #############################################################################

  def self.scan_users
    if @verbose
      verb = @config_file.exist? ? "rescan" : "scan"
    else
      verb = "%*s" % [$action_width, (@config_file.exist? ? "rescan" : "scan").upcase]
    end
    puts "#{verb} pch candidates (total=#{@users.size}, #{file_types(@users.keys)})"
    @users.each_pair { |k,v| scan_user(k,v) }
  end

  def self.scan_user(user, indirect)
    found = nil
    input = indirect ? indirect : user
    File.open(input) do |f|
      f.each_line do |line|
        line.force_encoding("UTF-8")
        next if !@scan_include_re.match(line)
        @scan_candidates.each do |pair|
          (dir,header) = *pair
          next unless $1 == header
          found = dir ? "#{dir}/#{$1}" : $1
          break
        end
        break if found
      end
    end
    return unless found
    @db_scan.store(user, found)
  rescue StandardError => e
    puts "WARNING: unable to read #{input}: #{e}"
  end

  #############################################################################

  class Info
    attr_accessor :language
    attr_accessor :use_flags
    attr_accessor :extra_flags
    attr_accessor :pretty_flags
  end

  def self.info_for_user(user, ofile)
    f = Info.new
    if c?(:USE_PRECOMPILED_HEADERS) && !%r{src/common/iso639_language_list.cpp}.match(user)
      user = Pathname.new(user).cleanpath.to_s
      f.language = "c++-header" if user.end_with?(".h")
      header = @db_scan.fetch(user, nil)
      f.use_flags = header ? " -include #{header}" : nil
      f.extra_flags = @htrace ? "-H" : nil
      f.pretty_flags = (@pretty || @htrace) ? {
        htrace: @htrace ? (ofile + ".htrace") : nil,
        user: @users.has_key?(user),
        precompile: f.use_flags != nil,
      } : nil
    end
    f
  end

  #############################################################################

  def self.moc_users(others)
    others.each { |other| @moc_users.store(other, nil) }
  end

  #############################################################################

  def self.make_task_filter(name)
    return nil unless @htrace

    lambda do |code,lines|
      rx_stat = /^([.]*[!x]?)\s(.*)/
      rx_ignore_begin = /^Multiple include guards may be useful for/i
      rx_ignore_more = /\//

      ignore = false

      File.open(name + ".htrace", "w") do |io|
        lines.each do |line|
          case
          when line =~ rx_stat
            ;
          when !ignore && line =~ rx_ignore_begin
            ignore = true
            next
          when ignore && line =~ rx_ignore_more
            next
          else
            puts line
            next
          end
          io.write("#{line}")
        end
      end
    end
  end

  #############################################################################

  def self.load_config
    # puts "load #{@config_file}" if @verbose
    config = {}
    config = @config_file.open { |f| JSON.load(f) } if @config_file.exist?
    @htrace = config.fetch('htrace', @htrace)
    @pretty = config.fetch('pretty', @pretty)
    @db_scan = config.fetch('scan', @db_scan)
  end

  def self.save_config
    # puts "save #{@config_file}" if @verbose
    root = {
      htrace: @htrace,
      pretty: @pretty,
      scan: @db_scan,
    }
    @config_file.open("w") do |out|
      out.write(JSON.generate(root, space:" ", indent:"  ", object_nl:"\n"))
      out.write("\n")
    end
  end

  #############################################################################

  def self.clean_patterns
    %W[
      **/*.[gp]ch **/*.pch-????????
      **/*.htrace
      #{@config_file}
    ]
  end

  #############################################################################
  #
  # Execute a system command, similar to helpers.rb/runq but differs mainly
  # in offering generator-style filter model, delayed, and in some cases
  # more information on normal rake stdout.
  #
  # All output is accumulated into a buffer until the command completes.
  # Advantage of keeping command-output together in a parallel build.
  # Disadvantage of delaying output until command is complete.
  #
  def self.runq(action, subject, command, options={})
    command = command.gsub(/\n/, ' ').gsub(/^\s+/, '').gsub(/\s+$/, '').gsub(/\s+/, ' ')
    if @verbose
      puts command
    else
      h = options.fetch(:htrace, nil) ? 't' : '-'
      u = options.fetch(:user, nil) ? 'u' : '-'
      p = options.fetch(:precompile, nil) ? 'p' : '-'
      puts "%c%c%c %*s %s" % [h, u, p, $action_width - 4, action.gsub(/ +/, '_').upcase, subject]
    end
    htrace = options.fetch(:htrace, nil)
    return execute(command, options) unless htrace

    # log -H output, return other
    File.open(htrace, "w") do |io|
      rx_stat = /^([.]*[!x]?)\s(.*)/
      rx_ignore_begin = /^Multiple include guards may be useful for/i
      rx_ignore_more = /\//
      ignore = false
      # generator records (sends to rake output) only lines we return
      execute(command, options) do |line|
        case
        when line =~ rx_stat
          io.write("#{line}")
          next nil
        when !ignore && line =~ rx_ignore_begin
          ignore = true
          next nil
        when ignore && line =~ rx_ignore_more
          next nil
        else
          next line
        end
      end
    end
  end

  def self.execute(command, options={}, &block)
    if STDOUT.tty? && $pty_module_available
      execute_tty(command, options, &block)
    else
      execute_command(command, options, &block)
    end
  end

  def self.execute_command(command, options={})
    lines = []
    IO.popen(command, :err => [:child, :out]) do |io|
      if block_given?
        io.each_line { |s| lines.push(s) if s = yield(s) }
      else
        io.each_line { |s| lines << s }
      end
    end
    end_execute(lines, options)
  end

  def self.execute_tty(command, options={})
    lines = []
    IO.pipe do |read,write|
      master,slave = PTY.open
      begin
        pid = spawn(command, :in => read, [:out, :err] => slave)
        read.close
        slave.close
        begin
          if block_given?
            master.each_line { |s| lines.push(s) if s = yield(s) }
          else
            master.each_line { |s| lines << s }
          end
        rescue
          ;
        ensure
          Process.wait(pid)
        end
      ensure
        master.close unless master.closed?
        slave.close unless slave.closed?
      end
    end
    end_execute(lines, options)
  end

  def self.end_execute(lines, options)
    if options.fetch(:allow_failure, nil) != true
      exit(1) if $?.exitstatus == nil
      exit($?.exitstatus) if $?.exitstatus != 0
    end
    ps = $?.clone
    puts lines
    [ps, lines]
  end

  #############################################################################

  # True when tracing headers during compilation. This controls when a filter
  # in CXX compilation action engages, and also causes adds -H compiler flag.
  @htrace = false

  # True when PCH enhances typical rake output with extra information.
  @pretty = false

  # GCC and clang differ in their extension preferences for precompiled
  # headers. clang has some knowledge of gch and at the time of this writing,
  # actually does search for both { .pch, .gch } but it's not documented.
  @extension = c(:COMPILER_TYPE) == "clang" ? ".pch" : ".gch"

  # Cache global verbose flag.
  @verbose = $verbose

  #  Each pch file must be unambiguous for simple scanning techniques.
  #  The scanner needs to know the string of a pch file as it is used in
  #  CPP include directives. It also needs to know the prefix pathname
  #  as it exists in source tree, relative to Rakefile.
  @scan_candidates = [ ["src", "common/common_pch.h"] ]

  # All usage of project pch files are expected to be between quotes,
  # and not angle brackets.
  @scan_include_re = /^\s*#\s*include\s+"([^"]+)"/

  # Location of pch configuration.
  @config_file = Pathname.new("config.pch.json")

  # Populated by project rakefile.
  @moc_users = {}

  # All candidate user files (.cpp, .moc)
  # user => intermediary
  # where user is the actual source file being compiled
  #   and intermediary is nil or alternate file to scan (used by .moc)
  @users = {}

  # Resident scan DB. Has mapping of user files (.cpp, .moc) to a pch file
  # (as used in #include directive).
  @db_scan = {}

  rself = Pathname.new(__FILE__)
  rself = rself.relative_path_from(Pathname.new(Dir.pwd)) unless rself.relative?
  @config_deps = [Rake.application.rakefile, 'build-config', rself.to_s]

  #############################################################################

  @endblock_skip_save = false

  END {
    next if @endblock_skip_save
    begin
      save_config
    rescue StandardError => e
      puts "WARNING: failed to save config: #{e}"
    end
  }

  #############################################################################

end # module PCH