# 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 'digest' require 'fileutils' require 'pathname' require 'getoptlong' SCRIPT_NAME = File.basename($0) COMMENT_REGEXP = /\/\// def usage(message) if message puts "Error: #{message}" puts end 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 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 "--print-all-sources Print all sources rather than generating sources" puts "--generate-xcfilelists Generate .xcfilelist files" puts "--input-xcfilelist-path Path of the generated input .xcfilelist file" puts "--output-xcfilelist-path Path of the generated output .xcfilelist file" puts puts "Generation options:" puts "--max-cpp-bundle-count Use global sequential numbers for cpp bundle filenames and set the limit on the number" puts "--max-obj-c-bundle-count Use global sequential numbers for Obj-C bundle filenames and set the limit on the number" puts "--dense-bundle-filter Densely bundle files matching the given path glob" exit 1 end MAX_BUNDLE_SIZE = 8 MAX_DENSE_BUNDLE_SIZE = 64 $derivedSourcesPath = nil $unifiedSourceOutputPath = nil $sourceTreePath = nil $verbose = false $mode = :GenerateBundles $inputXCFilelistPath = nil $outputXCFilelistPath = nil $maxCppBundleCount = nil $maxObjCBundleCount = nil $denseBundleFilters = [] 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], ['--print-bundled-sources', GetoptLong::NO_ARGUMENT], ['--print-all-sources', GetoptLong::NO_ARGUMENT], ['--generate-xcfilelists', GetoptLong::NO_ARGUMENT], ['--input-xcfilelist-path', GetoptLong::REQUIRED_ARGUMENT], ['--output-xcfilelist-path', GetoptLong::REQUIRED_ARGUMENT], ['--max-cpp-bundle-count', GetoptLong::REQUIRED_ARGUMENT], ['--max-obj-c-bundle-count', GetoptLong::REQUIRED_ARGUMENT], ['--dense-bundle-filter', GetoptLong::REQUIRED_ARGUMENT]).each { | opt, arg | case opt when '--help' usage(nil) when '--verbose' $verbose = true when '--derived-sources-path' $derivedSourcesPath = Pathname.new(arg) when '--source-tree-path' $sourceTreePath = Pathname.new(arg) usage("Source tree #{arg} does not exist.") if !$sourceTreePath.exist? when '--print-bundled-sources' $mode = :PrintBundledSources when '--print-all-sources' $mode = :PrintAllSources when '--generate-xcfilelists' $mode = :GenerateXCFilelists when '--input-xcfilelist-path' $inputXCFilelistPath = arg when '--output-xcfilelist-path' $outputXCFilelistPath = arg when '--max-cpp-bundle-count' $maxCppBundleCount = arg.to_i when '--max-obj-c-bundle-count' $maxObjCBundleCount = arg.to_i when '--dense-bundle-filter' $denseBundleFilters.push(arg) end } $unifiedSourceOutputPath = $derivedSourcesPath + Pathname.new("unified-sources") FileUtils.mkpath($unifiedSourceOutputPath) if !$unifiedSourceOutputPath.exist? && $mode != :GenerateXCFilelists usage("--derived-sources-path must be specified.") if !$unifiedSourceOutputPath usage("--source-tree-path must be specified.") if !$sourceTreePath log("Putting unified sources in #{$unifiedSourceOutputPath}") usage("At least one source list file must be specified.") 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 = [] $inputSources = [] $outputSources = [] 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 == :GenerateXCFilelists if derived? ($derivedSourcesPath + @path).to_s else '$(SRCROOT)/' + @path.to_s end elsif $mode == :GenerateBundles || !derived? @path.to_s else ($derivedSourcesPath + @path).to_s end end end class BundleManager attr_reader :bundleCount, :extension, :fileCount, :currentBundleText, :maxCount, :extraFiles def initialize(extension, max) @extension = extension @fileCount = 0 @bundleCount = 0 @currentBundleText = "" @maxCount = max @extraFiles = [] @currentDirectory = nil @lastBundlingPrefix = nil end def writeFile(file, text) bundleFile = $unifiedSourceOutputPath + file if $mode == :GenerateXCFilelists $outputSources << bundleFile return end if (!bundleFile.exist? || IO::read(bundleFile) != @currentBundleText) log("Writing bundle #{bundleFile} with: \n#{@currentBundleText}") IO::write(bundleFile, @currentBundleText) end end def bundleFileName() id = if @maxCount @bundleCount.to_s else # The dash makes the filenames more clear when using a hash. hash = Digest::SHA1.hexdigest(@currentDirectory.to_s)[0..7] "-#{hash}-#{@bundleCount}" end @extension == "cpp" ? "UnifiedSource#{id}.#{extension}" : "UnifiedSource#{id}-#{extension}.#{extension}" end def flush @bundleCount += 1 bundleFile = bundleFileName $generatedSources << $unifiedSourceOutputPath + bundleFile @extraFiles << bundleFile if @maxCount and @bundleCount > @maxCount writeFile(bundleFile, @currentBundleText) @currentBundleText = "" @fileCount = 0 end def flushToMax raise if !@maxCount while @bundleCount < @maxCount flush end end def addFile(sourceFile) path = sourceFile.path raise "wrong extension: #{path.extname} expected #{@extension}" unless path.extname == ".#{@extension}" bundlePrefix, bundleSize = BundlePrefixAndSizeForPath(path) if (@lastBundlingPrefix != bundlePrefix) log("Flushing because new top level directory; old: #{@currentDirectory}, new: #{path.dirname}") flush @lastBundlingPrefix = bundlePrefix @currentDirectory = path.dirname @bundleCount = 0 unless @maxCount end if @fileCount >= bundleSize log("Flushing because new bundle is full (#{@fileCount} sources)") flush end @currentBundleText += "#include \"#{sourceFile}\"\n" @fileCount += 1 end end def BundlePrefixAndSizeForPath(path) topLevelDirectory = TopLevelDirectoryForPath(path.dirname) $denseBundleFilters.each { |filter| if path.fnmatch(filter) return filter, MAX_DENSE_BUNDLE_SIZE end } return topLevelDirectory, MAX_BUNDLE_SIZE end def TopLevelDirectoryForPath(path) if !path return nil end while path.dirname != path.dirname.dirname path = path.dirname end return path end def ProcessFileForUnifiedSourceGeneration(sourceFile) path = sourceFile.path $inputSources << sourceFile.to_s bundle = $bundleManagers[path.extname] if !bundle log("No bundle for #{path.extname} files, building #{path} standalone") $generatedSources << sourceFile elsif !sourceFile.unifiable log("Not allowed to unify #{path}, building 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 = [] 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! next if line.empty? if seen[line] next if $mode == :GenerateXCFilelists raise "duplicate line: #{line} in #{path}" end seen[line] = true result << SourceFile.new(line, sourceFileIndex) } log("Found #{result.length} source files in #{path}") sourceFiles += result } log("Found sources: #{sourceFiles.sort}") sourceFiles.sort.each { | sourceFile | case $mode when :GenerateBundles, :GenerateXCFilelists ProcessFileForUnifiedSourceGeneration(sourceFile) when :PrintAllSources $generatedSources << sourceFile when :PrintBundledSources $generatedSources << sourceFile if $bundleManagers[sourceFile.path.extname] && sourceFile.unifiable end } if $mode != :PrintAllSources $bundleManagers.each_value { | manager | manager.flush maxCount = manager.maxCount next if !maxCount manager.flushToMax unless manager.extraFiles.empty? extension = manager.extension bundleCount = manager.bundleCount filesToAdd = manager.extraFiles.join(", ") raise "number of bundles for #{extension} sources, #{bundleCount}, exceeded limit, #{maxCount}. Please add #{filesToAdd} to Xcode then update UnifiedSource#{extension.capitalize}FileCount" end } end if $mode == :GenerateXCFilelists IO::write($inputXCFilelistPath, $inputSources.sort.join("\n") + "\n") if $inputXCFilelistPath IO::write($outputXCFilelistPath, $outputSources.sort.join("\n") + "\n") if $outputXCFilelistPath end # We use stdout to report our unified source list to CMake. # Add trailing semicolon and avoid a trailing newline for CMake's sake. log($generatedSources.join(";") + ";") print($generatedSources.join(";") + ";")