# 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] ..." puts " 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(";") + ";")