| # Copyright (C) 2017 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| # THE POSSIBILITY OF SUCH DAMAGE. |
| |
| require 'fileutils' |
| require 'pathname' |
| require 'getoptlong' |
| |
| SCRIPT_NAME = File.basename($0) |
| COMMENT_REGEXP = /\/\// |
| |
| def usage |
| puts "usage: #{SCRIPT_NAME} [options] <sources-list-file>..." |
| puts "<sources-list-file> may be separate arguments or one semicolon separated string" |
| puts "--help (-h) Print this message" |
| puts "--verbose (-v) Adds extra logging to stderr." |
| puts "Required arguments:" |
| puts "--source-tree-path (-s) Path to the root of the source directory." |
| puts "--derived-sources-path (-d) Path to the directory where the unified source files should be placed." |
| puts |
| puts "Optional arguments:" |
| puts "--print-bundled-sources Print bundled sources rather than generating sources" |
| puts "--feature-flags (-f) Space or semicolon separated list of enabled feature flags" |
| puts |
| puts "Generation options:" |
| puts "--max-cpp-bundle-count Sets the limit on the number of cpp bundles that can be generated" |
| puts "--max-obj-c-bundle-count Sets the limit on the number of Obj-C bundles that can be generated" |
| exit 1 |
| end |
| |
| MAX_BUNDLE_SIZE = 8 |
| $derivedSourcesPath = nil |
| $unifiedSourceOutputPath = nil |
| $sourceTreePath = nil |
| $featureFlags = {} |
| $verbose = false |
| $mode = :GenerateBundles |
| $maxCppBundleCount = nil |
| $maxObjCBundleCount = nil |
| |
| def log(text) |
| $stderr.puts text if $verbose |
| end |
| |
| GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT], |
| ['--verbose', '-v', GetoptLong::NO_ARGUMENT], |
| ['--derived-sources-path', '-d', GetoptLong::REQUIRED_ARGUMENT], |
| ['--source-tree-path', '-s', GetoptLong::REQUIRED_ARGUMENT], |
| ['--feature-flags', '-f', GetoptLong::REQUIRED_ARGUMENT], |
| ['--print-bundled-sources', GetoptLong::NO_ARGUMENT], |
| ['--max-cpp-bundle-count', GetoptLong::REQUIRED_ARGUMENT], |
| ['--max-obj-c-bundle-count', GetoptLong::REQUIRED_ARGUMENT]).each { |
| | opt, arg | |
| case opt |
| when '--help' |
| usage |
| when '--verbose' |
| $verbose = true |
| when '--derived-sources-path' |
| $derivedSourcesPath = Pathname.new(arg) |
| $unifiedSourceOutputPath = $derivedSourcesPath + Pathname.new("unified-sources") |
| FileUtils.mkpath($unifiedSourceOutputPath) if !$unifiedSourceOutputPath.exist? |
| when '--source-tree-path' |
| $sourceTreePath = Pathname.new(arg) |
| usage if !$sourceTreePath.exist? |
| when '--feature-flags' |
| arg.gsub(/\s+/, ";").split(";").map { |x| $featureFlags[x] = true } |
| when '--print-bundled-sources' |
| $mode = :PrintBundledSources |
| when '--max-cpp-bundle-count' |
| $maxCppBundleCount = arg.to_i |
| when '--max-obj-c-bundle-count' |
| $maxObjCBundleCount = arg.to_i |
| end |
| } |
| |
| usage if !$unifiedSourceOutputPath || !$sourceTreePath |
| log("putting unified sources in #{$unifiedSourceOutputPath}") |
| log("Active Feature flags: #{$featureFlags.keys.inspect}") |
| |
| usage if ARGV.length == 0 |
| # Even though CMake will only pass us a single semicolon separated arguemnts, we separate all the arguments for simplicity. |
| sourceListFiles = ARGV.to_a.map { | sourceFileList | sourceFileList.split(";") }.flatten |
| log("source files: #{sourceListFiles}") |
| $generatedSources = [] |
| |
| class SourceFile |
| attr_reader :unifiable, :fileIndex, :path |
| def initialize(file, fileIndex) |
| @unifiable = true |
| @fileIndex = fileIndex |
| |
| attributeStart = file =~ /@/ |
| if attributeStart |
| # We want to make sure we skip the first @ so split works correctly |
| attributesText = file[(attributeStart + 1)..file.length] |
| attributesText.split(/\s*@/).each { |
| | attribute | |
| case attribute.strip |
| when "no-unify" |
| @unifiable = false |
| else |
| raise "unknown attribute: #{attribute}" |
| end |
| } |
| file = file[0..(attributeStart-1)] |
| end |
| |
| @path = Pathname.new(file.strip) |
| end |
| |
| def <=>(other) |
| return @path.dirname <=> other.path.dirname if @path.dirname != other.path.dirname |
| return @path.basename <=> other.path.basename if @fileIndex == other.fileIndex |
| @fileIndex <=> other.fileIndex |
| end |
| |
| def derived? |
| return @derived if @derived != nil |
| @derived = !($sourceTreePath + self.path).exist? |
| end |
| |
| def to_s |
| if $mode == :GenerateBundles || !derived? |
| @path.to_s |
| else |
| ($derivedSourcesPath + @path).to_s |
| end |
| end |
| end |
| |
| class BundleManager |
| attr_reader :bundleCount, :extension, :fileCount, :currentBundleText, :maxCount |
| |
| def initialize(extension, max) |
| @extension = extension |
| @fileCount = 0 |
| @bundleCount = 0 |
| @currentBundleText = "" |
| @maxCount = max |
| end |
| |
| def writeFile(file, text) |
| bundleFile = $unifiedSourceOutputPath + file |
| if (!bundleFile.exist? || IO::read(bundleFile) != @currentBundleText) |
| log("writing bundle #{bundleFile} with: \n#{@currentBundleText}") |
| IO::write(bundleFile, @currentBundleText) |
| end |
| end |
| |
| def bundleFileName(number) |
| @extension == "cpp" ? "UnifiedSource#{number}.#{extension}" : "UnifiedSource#{number}-#{extension}.#{extension}" |
| end |
| |
| def flush |
| # No point in writing an empty bundle file |
| return if @currentBundleText == "" |
| |
| @bundleCount += 1 |
| bundleFile = bundleFileName(@bundleCount) |
| $generatedSources << $unifiedSourceOutputPath + bundleFile |
| |
| writeFile(bundleFile, @currentBundleText) |
| @currentBundleText = "" |
| @fileCount = 0 |
| end |
| |
| def flushToMax |
| raise if !@maxCount |
| ((@bundleCount+1)..@maxCount).each { |
| | index | |
| writeFile(bundleFileName(index), "") |
| } |
| end |
| |
| def addFile(sourceFile) |
| path = sourceFile.path |
| raise "wrong extension: #{path.extname} expected #{@extension}" unless path.extname == ".#{@extension}" |
| if @fileCount == MAX_BUNDLE_SIZE |
| log("flushing because new bundle is full #{@fileCount}") |
| flush |
| end |
| @currentBundleText += "#include \"#{sourceFile}\"\n" |
| @fileCount += 1 |
| end |
| end |
| |
| def ProcessFileForUnifiedSourceGeneration(sourceFile) |
| path = sourceFile.path |
| if ($currentDirectory != path.dirname) |
| log("flushing because new dirname old: #{$currentDirectory}, new: #{path.dirname}") |
| $bundleManagers.each_value { |x| x.flush } |
| $currentDirectory = path.dirname |
| end |
| |
| bundle = $bundleManagers[path.extname] |
| if !bundle || !sourceFile.unifiable |
| log("No bundle for #{path.extname} files building #{path} standalone") |
| $generatedSources << sourceFile |
| else |
| bundle.addFile(sourceFile) |
| end |
| end |
| |
| $bundleManagers = { |
| ".cpp" => BundleManager.new("cpp", $maxCppBundleCount), |
| ".mm" => BundleManager.new("mm", $maxObjCBundleCount) |
| } |
| |
| seen = {} |
| sourceFiles = [] |
| |
| sourceListFiles.each_with_index { |
| | path, sourceFileIndex | |
| log("reading #{path}") |
| result = [] |
| inDisabledLines = false |
| File.read(path).lines.each { |
| | line | |
| commentStart = line =~ COMMENT_REGEXP |
| log("before: #{line}") |
| if commentStart != nil |
| line = line.slice(0, commentStart) |
| log("after: #{line}") |
| end |
| line.strip! |
| if line == "#endif" |
| inDisabledLines = false |
| next |
| end |
| |
| next if line.empty? || inDisabledLines |
| |
| if line =~ /\A#if/ |
| raise "malformed #if" unless line =~ /\A#if\s+(\S+)/ |
| inDisabledLines = !$featureFlags[$1] |
| else |
| raise "duplicate line: #{line} in #{path}" if seen[line] |
| seen[line] = true |
| result << SourceFile.new(line, sourceFileIndex) |
| end |
| } |
| raise "Couldn't find closing \"#endif\"" if inDisabledLines |
| |
| log("found #{result.length} source files in #{path}") |
| sourceFiles += result |
| } |
| |
| log("Found sources: #{sourceFiles.sort}") |
| |
| sourceFiles.sort.each { |
| | sourceFile | |
| case $mode |
| when :GenerateBundles |
| ProcessFileForUnifiedSourceGeneration(sourceFile) |
| when :PrintBundledSources |
| $generatedSources << sourceFile if $bundleManagers[sourceFile.path.extname] && sourceFile.unifiable |
| end |
| } |
| |
| $bundleManagers.each_value { |
| | manager | |
| manager.flush |
| |
| maxCount = manager.maxCount |
| next if !maxCount |
| |
| manager.flushToMax |
| bundleCount = manager.bundleCount |
| extension = manager.extension |
| if bundleCount > maxCount |
| filesToAdd = ((maxCount+1)..bundleCount).map { |x| manager.bundleFileName(x) }.join(", ") |
| raise "number of bundles for #{extension} sources, #{bundleCount}, exceeded limit, #{maxCount}. Please add #{filesToAdd} to Xcode then update UnifiedSource#{extension.capitalize}FileCount" |
| end |
| } |
| |
| # We use stdout to report our unified source list to CMake. |
| # Add trailing semicolon since CMake seems dislikes not having it. |
| # Also, make sure we use print instead of puts because CMake will think the \n is a source file and fail to build. |
| |
| log($generatedSources.join(";") + ";") |
| print($generatedSources.join(";") + ";") |