d352a7c8c575a677493a02ed02cb1c4677222a29
[WebKit-https.git] / Source / WTF / Scripts / generate-unified-source-bundles.rb
1 # Copyright (C) 2017 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1. Redistributions of source code must retain the above copyright
7 #    notice, this list of conditions and the following disclaimer.
8 # 2. Redistributions in binary form must reproduce the above copyright
9 #    notice, this list of conditions and the following disclaimer in the
10 #    documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
13 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
14 # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
15 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
16 # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
18 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
19 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
20 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
21 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
22 # THE POSSIBILITY OF SUCH DAMAGE.
23
24 require 'digest'
25 require 'fileutils'
26 require 'pathname'
27 require 'getoptlong'
28
29 SCRIPT_NAME = File.basename($0)
30 COMMENT_REGEXP = /\/\//
31
32 def usage(message)
33     if message
34         puts "Error: #{message}"
35         puts
36     end
37
38     puts "usage: #{SCRIPT_NAME} [options] <sources-list-file>..."
39     puts "<sources-list-file> may be separate arguments or one semicolon separated string"
40     puts "--help                          (-h) Print this message"
41     puts "--verbose                       (-v) Adds extra logging to stderr."
42     puts
43     puts "Required arguments:"
44     puts "--source-tree-path              (-s) Path to the root of the source directory."
45     puts "--derived-sources-path          (-d) Path to the directory where the unified source files should be placed."
46     puts
47     puts "Optional arguments:"
48     puts "--print-bundled-sources              Print bundled sources rather than generating sources"
49     puts "--print-all-sources                  Print all sources rather than generating sources"
50     puts "--generate-xcfilelists               Generate .xcfilelist files"
51     puts "--input-xcfilelist-path              Path of the generated input .xcfilelist file"
52     puts "--output-xcfilelist-path             Path of the generated output .xcfilelist file"
53     puts "--feature-flags                 (-f) Space or semicolon separated list of enabled feature flags"
54     puts
55     puts "Generation options:"
56     puts "--max-cpp-bundle-count               Use global sequential numbers for cpp bundle filenames and set the limit on the number"
57     puts "--max-obj-c-bundle-count             Use global sequential numbers for Obj-C bundle filenames and set the limit on the number"
58     exit 1
59 end
60
61 MAX_BUNDLE_SIZE = 8
62 $derivedSourcesPath = nil
63 $unifiedSourceOutputPath = nil
64 $sourceTreePath = nil
65 $featureFlags = {}
66 $verbose = false
67 $mode = :GenerateBundles
68 $inputXCFilelistPath = nil
69 $outputXCFilelistPath = nil
70 $maxCppBundleCount = nil
71 $maxObjCBundleCount = nil
72
73 def log(text)
74     $stderr.puts text if $verbose
75 end
76
77 GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT],
78                ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
79                ['--derived-sources-path', '-d', GetoptLong::REQUIRED_ARGUMENT],
80                ['--source-tree-path', '-s', GetoptLong::REQUIRED_ARGUMENT],
81                ['--feature-flags', '-f', GetoptLong::REQUIRED_ARGUMENT],
82                ['--print-bundled-sources', GetoptLong::NO_ARGUMENT],
83                ['--print-all-sources', GetoptLong::NO_ARGUMENT],
84                ['--generate-xcfilelists', GetoptLong::NO_ARGUMENT],
85                ['--input-xcfilelist-path', GetoptLong::REQUIRED_ARGUMENT],
86                ['--output-xcfilelist-path', GetoptLong::REQUIRED_ARGUMENT],
87                ['--max-cpp-bundle-count', GetoptLong::REQUIRED_ARGUMENT],
88                ['--max-obj-c-bundle-count', GetoptLong::REQUIRED_ARGUMENT]).each {
89     | opt, arg |
90     case opt
91     when '--help'
92         usage(nil)
93     when '--verbose'
94         $verbose = true
95     when '--derived-sources-path'
96         $derivedSourcesPath = Pathname.new(arg)
97     when '--source-tree-path'
98         $sourceTreePath = Pathname.new(arg)
99         usage("Source tree #{arg} does not exist.") if !$sourceTreePath.exist?
100     when '--feature-flags'
101         arg.gsub(/\s+/, ";").split(";").map { |x| $featureFlags[x] = true }
102     when '--print-bundled-sources'
103         $mode = :PrintBundledSources
104     when '--print-all-sources'
105         $mode = :PrintAllSources
106     when '--generate-xcfilelists'
107         $mode = :GenerateXCFilelists
108     when '--input-xcfilelist-path'
109         $inputXCFilelistPath = arg
110     when '--output-xcfilelist-path'
111         $outputXCFilelistPath = arg
112     when '--max-cpp-bundle-count'
113         $maxCppBundleCount = arg.to_i
114     when '--max-obj-c-bundle-count'
115         $maxObjCBundleCount = arg.to_i
116     end
117 }
118
119 $unifiedSourceOutputPath = $derivedSourcesPath + Pathname.new("unified-sources")
120 FileUtils.mkpath($unifiedSourceOutputPath) if !$unifiedSourceOutputPath.exist? && $mode != :GenerateXCFilelists
121
122 usage("--derived-sources-path must be specified.") if !$unifiedSourceOutputPath
123 usage("--source-tree-path must be specified.") if !$sourceTreePath
124 log("Putting unified sources in #{$unifiedSourceOutputPath}")
125 log("Active Feature flags: #{$featureFlags.keys.inspect}")
126
127 usage("At least one source list file must be specified.") if ARGV.length == 0
128 # Even though CMake will only pass us a single semicolon separated arguemnts, we separate all the arguments for simplicity.
129 sourceListFiles = ARGV.to_a.map { | sourceFileList | sourceFileList.split(";") }.flatten
130 log("Source files: #{sourceListFiles}")
131 $generatedSources = []
132 $inputSources = []
133 $outputSources = []
134
135 class SourceFile
136     attr_reader :unifiable, :fileIndex, :path
137     def initialize(file, fileIndex)
138         @unifiable = true
139         @fileIndex = fileIndex
140
141         attributeStart = file =~ /@/
142         if attributeStart
143             # We want to make sure we skip the first @ so split works correctly
144             attributesText = file[(attributeStart + 1)..file.length]
145             attributesText.split(/\s*@/).each {
146                 | attribute |
147                 case attribute.strip
148                 when "no-unify"
149                     @unifiable = false
150                 else
151                     raise "unknown attribute: #{attribute}"
152                 end
153             }
154             file = file[0..(attributeStart-1)]
155         end
156
157         @path = Pathname.new(file.strip)
158     end
159
160     def <=>(other)
161         return @path.dirname <=> other.path.dirname if @path.dirname != other.path.dirname
162         return @path.basename <=> other.path.basename if @fileIndex == other.fileIndex
163         @fileIndex <=> other.fileIndex
164     end
165
166     def derived?
167         return @derived if @derived != nil
168         @derived = !($sourceTreePath + self.path).exist?
169     end
170
171     def to_s
172         if $mode == :GenerateXCFilelists
173             if derived?
174                 ($derivedSourcesPath + @path).to_s
175             else
176                 '$(SRCROOT)/' + @path.to_s
177             end
178         elsif $mode == :GenerateBundles || !derived?
179             @path.to_s
180         else
181             ($derivedSourcesPath + @path).to_s
182         end
183     end
184 end
185
186 class BundleManager
187     attr_reader :bundleCount, :extension, :fileCount, :currentBundleText, :maxCount, :extraFiles
188
189     def initialize(extension, max)
190         @extension = extension
191         @fileCount = 0
192         @bundleCount = 0
193         @currentBundleText = ""
194         @maxCount = max
195         @extraFiles = []
196         @currentDirectory = nil
197     end
198
199     def writeFile(file, text)
200         bundleFile = $unifiedSourceOutputPath + file
201         if $mode == :GenerateXCFilelists
202             $outputSources << bundleFile
203             return
204         end
205         if (!bundleFile.exist? || IO::read(bundleFile) != @currentBundleText)
206             log("Writing bundle #{bundleFile} with: \n#{@currentBundleText}")
207             IO::write(bundleFile, @currentBundleText)
208         end
209     end
210
211     def bundleFileName()
212         id =
213             if @maxCount
214                 @bundleCount.to_s
215             else
216                 # The dash makes the filenames more clear when using a hash.
217                 hash = Digest::SHA1.hexdigest(@currentDirectory.to_s)[0..7]
218                 "-#{hash}-#{@bundleCount}"
219             end
220         @extension == "cpp" ? "UnifiedSource#{id}.#{extension}" : "UnifiedSource#{id}-#{extension}.#{extension}"
221     end
222
223     def flush
224         @bundleCount += 1
225         bundleFile = bundleFileName
226         $generatedSources << $unifiedSourceOutputPath + bundleFile
227         @extraFiles << bundleFile if @maxCount and @bundleCount > @maxCount
228
229         writeFile(bundleFile, @currentBundleText)
230         @currentBundleText = ""
231         @fileCount = 0
232     end
233
234     def flushToMax
235         raise if !@maxCount
236         while @bundleCount < @maxCount
237             flush
238         end
239     end
240
241     def addFile(sourceFile)
242         path = sourceFile.path
243         raise "wrong extension: #{path.extname} expected #{@extension}" unless path.extname == ".#{@extension}"
244         if (TopLevelDirectoryForPath(@currentDirectory) != TopLevelDirectoryForPath(path.dirname))
245             log("Flushing because new top level directory; old: #{@currentDirectory}, new: #{path.dirname}")
246             flush
247             @currentDirectory = path.dirname
248             @bundleCount = 0 unless @maxCount
249         end
250         if @fileCount == MAX_BUNDLE_SIZE
251             log("Flushing because new bundle is full (#{@fileCount} sources)")
252             flush
253         end
254         @currentBundleText += "#include \"#{sourceFile}\"\n"
255         @fileCount += 1
256     end
257 end
258
259 def TopLevelDirectoryForPath(path)
260     if !path
261         return nil
262     end
263     while path.dirname != path.dirname.dirname
264         path = path.dirname
265     end
266     return path
267 end
268
269 def ProcessFileForUnifiedSourceGeneration(sourceFile)
270     path = sourceFile.path
271     $inputSources << sourceFile.to_s
272
273     bundle = $bundleManagers[path.extname]
274     if !bundle
275         log("No bundle for #{path.extname} files, building #{path} standalone")
276         $generatedSources << sourceFile
277     elsif !sourceFile.unifiable
278         log("Not allowed to unify #{path}, building standalone")
279         $generatedSources << sourceFile
280     else
281         bundle.addFile(sourceFile)
282     end
283 end
284
285 $bundleManagers = {
286     ".cpp" => BundleManager.new("cpp", $maxCppBundleCount),
287     ".mm" => BundleManager.new("mm", $maxObjCBundleCount)
288 }
289
290 seen = {}
291 sourceFiles = []
292
293 sourceListFiles.each_with_index {
294     | path, sourceFileIndex |
295     log("Reading #{path}")
296     result = []
297     inDisabledLines = false
298     File.read(path).lines.each {
299         | line |
300         commentStart = line =~ COMMENT_REGEXP
301         log("Before: #{line}")
302         if commentStart != nil
303             line = line.slice(0, commentStart)
304             log("After: #{line}")
305         end
306         line.strip!
307         if line == "#endif"
308             inDisabledLines = false
309             next
310         end
311
312         next if line.empty? || inDisabledLines
313
314         if line =~ /\A#if/
315             raise "malformed #if" unless line =~ /\A#if\s+(\S+)/
316             inDisabledLines = !$featureFlags[$1]
317         else
318             if seen[line]
319                 next if $mode == :GenerateXCFilelists
320                 raise "duplicate line: #{line} in #{path}"
321             end
322             seen[line] = true
323             result << SourceFile.new(line, sourceFileIndex)
324         end
325     }
326     raise "Couldn't find closing \"#endif\"" if inDisabledLines
327
328     log("Found #{result.length} source files in #{path}")
329     sourceFiles += result
330 }
331
332 log("Found sources: #{sourceFiles.sort}")
333
334 sourceFiles.sort.each {
335     | sourceFile |
336     case $mode
337     when :GenerateBundles, :GenerateXCFilelists
338         ProcessFileForUnifiedSourceGeneration(sourceFile)
339     when :PrintAllSources
340         $generatedSources << sourceFile
341     when :PrintBundledSources
342         $generatedSources << sourceFile if $bundleManagers[sourceFile.path.extname] && sourceFile.unifiable
343     end
344 }
345
346 if $mode != :PrintAllSources
347     $bundleManagers.each_value {
348         | manager |
349         manager.flush
350
351         maxCount = manager.maxCount
352         next if !maxCount
353
354         manager.flushToMax
355
356         unless manager.extraFiles.empty?
357             extension = manager.extension
358             bundleCount = manager.bundleCount
359             filesToAdd = manager.extraFiles.join(", ")
360             raise "number of bundles for #{extension} sources, #{bundleCount}, exceeded limit, #{maxCount}. Please add #{filesToAdd} to Xcode then update UnifiedSource#{extension.capitalize}FileCount"
361         end
362     }
363 end
364
365 if $mode == :GenerateXCFilelists
366     IO::write($inputXCFilelistPath, $inputSources.sort.join("\n") + "\n") if $inputXCFilelistPath
367     IO::write($outputXCFilelistPath, $outputSources.sort.join("\n") + "\n") if $outputXCFilelistPath
368 end
369
370 # We use stdout to report our unified source list to CMake.
371 # Add trailing semicolon and avoid a trailing newline for CMake's sake.
372
373 log($generatedSources.join(";") + ";")
374 print($generatedSources.join(";") + ";")