Setup WebCore build to start using unified sources.
[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 'fileutils'
25 require 'pathname'
26 require 'getoptlong'
27
28 SCRIPT_NAME = File.basename($0)
29 COMMENT_REGEXP = /\/\//
30
31 def usage
32     puts "usage: #{SCRIPT_NAME} [options] <sources-list-file>..."
33     puts "<sources-list-file> may be separate arguments or one semicolon separated string"
34     puts "--help                          (-h) Print this message"
35     puts "--verbose                       (-v) Adds extra logging to stderr."
36     puts "Required arguments:"
37     puts "--source-tree-path              (-s) Path to the root of the source directory."
38     puts "--derived-sources-path          (-d) Path to the directory where the unified source files should be placed."
39     puts
40     puts "Optional arguments:"
41     puts "--print-bundled-sources              Print bundled sources rather than generating sources"
42     puts "--feature-flags                 (-f) Space or semicolon separated list of enabled feature flags"
43     puts
44     puts "Generation options:"
45     puts "--max-cpp-bundle-count               Sets the limit on the number of cpp bundles that can be generated"
46     puts "--max-obj-c-bundle-count             Sets the limit on the number of Obj-C bundles that can be generated"
47     exit 1
48 end
49
50 MAX_BUNDLE_SIZE = 8
51 $derivedSourcesPath = nil
52 $unifiedSourceOutputPath = nil
53 $sourceTreePath = nil
54 $featureFlags = {}
55 $verbose = false
56 $mode = :GenerateBundles
57 $maxCppBundleCount = nil
58 $maxObjCBundleCount = nil
59
60 def log(text)
61     $stderr.puts text if $verbose
62 end
63
64 GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT],
65                ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
66                ['--derived-sources-path', '-d', GetoptLong::REQUIRED_ARGUMENT],
67                ['--source-tree-path', '-s', GetoptLong::REQUIRED_ARGUMENT],
68                ['--feature-flags', '-f', GetoptLong::REQUIRED_ARGUMENT],
69                ['--print-bundled-sources', GetoptLong::NO_ARGUMENT],
70                ['--max-cpp-bundle-count', GetoptLong::REQUIRED_ARGUMENT],
71                ['--max-obj-c-bundle-count', GetoptLong::REQUIRED_ARGUMENT]).each {
72     | opt, arg |
73     case opt
74     when '--help'
75         usage
76     when '--verbose'
77         $verbose = true
78     when '--derived-sources-path'
79         $derivedSourcesPath = Pathname.new(arg)
80         $unifiedSourceOutputPath = $derivedSourcesPath + Pathname.new("unified-sources")
81         FileUtils.mkpath($unifiedSourceOutputPath) if !$unifiedSourceOutputPath.exist?
82     when '--source-tree-path'
83         $sourceTreePath = Pathname.new(arg)
84         usage if !$sourceTreePath.exist?
85     when '--feature-flags'
86         arg.gsub(/\s+/, ";").split(";").map { |x| $featureFlags[x] = true }
87     when '--print-bundled-sources'
88         $mode = :PrintBundledSources
89     when '--max-cpp-bundle-count'
90         $maxCppBundleCount = arg.to_i
91     when '--max-obj-c-bundle-count'
92         $maxObjCBundleCount = arg.to_i
93     end
94 }
95
96 usage if !$unifiedSourceOutputPath || !$sourceTreePath
97 log("putting unified sources in #{$unifiedSourceOutputPath}")
98 log("Active Feature flags: #{$featureFlags.keys.inspect}")
99
100 usage if ARGV.length == 0
101 # Even though CMake will only pass us a single semicolon separated arguemnts, we separate all the arguments for simplicity.
102 sourceListFiles = ARGV.to_a.map { | sourceFileList | sourceFileList.split(";") }.flatten
103 log("source files: #{sourceListFiles}")
104 $generatedSources = []
105
106 class SourceFile
107     attr_reader :unifiable, :fileIndex, :path
108     def initialize(file, fileIndex)
109         @unifiable = true
110         @fileIndex = fileIndex
111
112         attributeStart = file =~ /@/
113         if attributeStart
114             # We want to make sure we skip the first @ so split works correctly
115             attributesText = file[(attributeStart + 1)..file.length]
116             attributesText.split(/\s*@/).each {
117                 | attribute |
118                 case attribute.strip
119                 when "no-unify"
120                     @unifiable = false
121                 else
122                     raise "unknown attribute: #{attribute}"
123                 end
124             }
125             file = file[0..(attributeStart-1)]
126         end
127
128         @path = Pathname.new(file.strip)
129     end
130
131     def <=>(other)
132         return @path.dirname <=> other.path.dirname if @path.dirname != other.path.dirname
133         return @path.basename <=> other.path.basename if @fileIndex == other.fileIndex
134         @fileIndex <=> other.fileIndex
135     end
136
137     def derived?
138         return @derived if @derived != nil
139         @derived = !($sourceTreePath + self.path).exist?
140     end
141
142     def to_s
143         if $mode == :GenerateBundles || !derived?
144             @path.to_s
145         else
146             ($derivedSourcesPath + @path).to_s
147         end
148     end
149 end
150
151 class BundleManager
152     attr_reader :bundleCount, :extension, :fileCount, :currentBundleText, :maxCount
153
154     def initialize(extension, max)
155         @extension = extension
156         @fileCount = 0
157         @bundleCount = 0
158         @currentBundleText = ""
159         @maxCount = max
160     end
161
162     def writeFile(file, text)
163         bundleFile = $unifiedSourceOutputPath + file
164         if (!bundleFile.exist? || IO::read(bundleFile) != @currentBundleText)
165             log("writing bundle #{bundleFile} with: \n#{@currentBundleText}")
166             IO::write(bundleFile, @currentBundleText)
167         end
168     end
169
170     def bundleFileName(number)
171         "UnifiedSource#{number}.#{extension}"
172     end
173
174     def flush
175         # No point in writing an empty bundle file
176         return if @currentBundleText == ""
177
178         @bundleCount += 1
179         bundleFile = bundleFileName(@bundleCount)
180         $generatedSources << $unifiedSourceOutputPath + bundleFile
181
182         writeFile(bundleFile, @currentBundleText)
183         @currentBundleText = ""
184         @fileCount = 0
185     end
186
187     def flushToMax
188         raise if !@maxCount
189         ((@bundleCount+1)..@maxCount).each {
190             | index |
191             writeFile(bundleFileName(index), "")
192         }
193     end
194
195     def addFile(sourceFile)
196         path = sourceFile.path
197         raise "wrong extension: #{path.extname} expected #{@extension}" unless path.extname == ".#{@extension}"
198         if @fileCount == MAX_BUNDLE_SIZE
199             log("flushing because new bundle is full #{@fileCount}")
200             flush
201         end
202         @currentBundleText += "#include \"#{sourceFile}\"\n"
203         @fileCount += 1
204     end
205 end
206
207 def ProcessFileForUnifiedSourceGeneration(sourceFile)
208     path = sourceFile.path
209     if ($currentDirectory != path.dirname)
210         log("flushing because new dirname old: #{$currentDirectory}, new: #{path.dirname}")
211         $bundleManagers.each_value { |x| x.flush }
212         $currentDirectory = path.dirname
213     end
214
215     bundle = $bundleManagers[path.extname]
216     if !bundle || !sourceFile.unifiable
217         log("No bundle for #{path.extname} files building #{path} standalone")
218         $generatedSources << sourceFile
219     else
220         bundle.addFile(sourceFile)
221     end
222 end
223
224 $bundleManagers = {
225     ".cpp" => BundleManager.new("cpp", $maxCppBundleCount),
226     ".mm" => BundleManager.new("mm", $maxObjCBundleCount)
227 }
228
229 seen = {}
230 sourceFiles = []
231
232 sourceListFiles.each_with_index {
233     | path, sourceFileIndex |
234     log("reading #{path}")
235     result = []
236     inDisabledLines = false
237     File.read(path).lines.each {
238         | line |
239         commentStart = line =~ COMMENT_REGEXP
240         log("before: #{line}")
241         if commentStart != nil
242             line = line.slice(0, commentStart)
243             log("after: #{line}")
244         end
245         line.strip!
246         if line == "#endif"
247             inDisabledLines = false
248             next
249         end
250
251         next if line.empty? || inDisabledLines
252
253         if line =~ /\A#if/
254             raise "malformed #if" unless line =~ /\A#if\s+(\S+)/
255             inDisabledLines = !$featureFlags[$1]
256         else
257             raise "duplicate line: #{line} in #{path}" if seen[line]
258             seen[line] = true
259             result << SourceFile.new(line, sourceFileIndex)
260         end
261     }
262     raise "Couldn't find closing \"#endif\"" if inDisabledLines
263
264     log("found #{result.length} source files in #{path}")
265     sourceFiles += result
266 }
267
268 log("Found sources: #{sourceFiles.sort}")
269
270 sourceFiles.sort.each {
271     | sourceFile |
272     case $mode
273     when :GenerateBundles
274         ProcessFileForUnifiedSourceGeneration(sourceFile)
275     when :PrintBundledSources
276         $generatedSources << sourceFile if $bundleManagers[sourceFile.path.extname] && sourceFile.unifiable
277     end
278 }
279
280 $bundleManagers.each_value {
281     | manager |
282     manager.flush
283
284     maxCount = manager.maxCount
285     next if !maxCount
286
287     manager.flushToMax
288     bundleCount = manager.bundleCount
289     extension = manager.extension
290     if bundleCount > maxCount
291         filesToAdd = ((maxCount+1)..bundleCount).map { |x| manager.bundleFileName(x) }.join(", ")
292         raise "number of bundles for #{extension} sources, #{bundleCount}, exceeded limit, #{maxCount}. Please add #{filesToAdd} to Xcode then update UnifiedSource#{extension.capitalize}FileCount"
293     end
294 }
295
296 # We use stdout to report our unified source list to CMake.
297 # Add trailing semicolon since CMake seems dislikes not having it.
298 # Also, make sure we use print instead of puts because CMake will think the \n is a source file and fail to build.
299
300 log($generatedSources.join(";") + ";")
301 print($generatedSources.join(";") + ";")