Files
2025-09-29 00:52:08 +02:00

754 lines
25 KiB
Ruby
Executable File

#
# File:: unity_build.rb
#
# Description:: Supports a unity build side by side with project builder.
# - Loads .unity config file.
# - Creates new cpp files ( unity files )
# - 'Excludes' files from build inside the project format - this is required to be set for all platforms and build configs.
# - Creates CL for submission to p4 of new unity files.
# - Tidies p4 of old unity files as it goes.
# - Monkey patches into project builder classes.
# - Tests by importing a non unity VS2008 vcproj.
# - handles unity prologue and epilogue about each included file
#
# Author:: Derek Ward <derek@rockstarnorth.com>
# Date:: 31 January 2011
#
#
#-----------------------------------------------------------------------------
# Uses
#-----------------------------------------------------------------------------
require "fileutils"
require 'pipeline/os/path'
require 'pipeline/os/getopt'
require 'pipeline/log/log'
require "pipeline/coding/projbuild/generators/internal"
require "pipeline/coding/projbuild/generators/vs2008"
require "pipeline/coding/projbuild/generators/rageprojbuilder"
require "pipeline/coding/projbuild"
#-----------------------------------------------------------------------------
# Implementation
#-----------------------------------------------------------------------------
module Pipeline
module ProjBuild
#####################################################
# A unity file
# represents the definition of a new file added to
# the project to compile larger units of compilation.
# - serialisable.
# - for cpp files
class UnityFile
FOLDER_TOKEN = "_folder_"
EMPTY_REGEXP = "^(((\.*|\w:)\/)*)(#{FOLDER_TOKEN})\/(.*)((\\.cpp)|(\\.c))$"
#"^(\.*|\w:)\/(#{FOLDER_TOKEN})\/(((?!exclusions_can\.|go_here\.).)*)(\.)(cpp)$"
EMPTY_REGEXP_OPTIONS = "i"
UNITY_BUILD_DEFINE = "__UNITYBUILD"
UNITY_PREFIX = "_unity_"
attr_reader :name # unique identifier.
attr_reader :unity_folder # the folder where the unity file resides.
attr_reader :project_folder # the project folder the unity file is being built for.
attr_reader :project # the project the unity file is being built for.
attr_reader :regexp # regexp filter for files.
attr_reader :files # a list of files for unity inclusion.
def initialize( name, unity_folder, project, project_folder )
@name = name
@unity_folder = unity_folder
@project = project
@project_folder = project_folder
@files = []
@regexps = []
end
# relative paths are used to make the project portable.
def relative_path( path )
rel_path = "../" # because the unity files are in their own folder, this takes us up to the folder of the project file - the unity folder is always relative to the project.
# now calculate where the source files would be located relative to this path
# this is the project folder minus the full path
proj_folders = @project_folder.split("/")
path_folders = path.split("/")
same = true
remaining_path,common_path = "",""
proj_folders.each_with_index do |proj_folder, idx|
if (same and (proj_folder == path_folders[idx]) )
common_path += "#{proj_folder}/"
else
same = false
remaining_path += "#{proj_folder}/"
end
end
# count the folders left in path, add ../ to relative path for each.
remaining_path.scan("/").length.times { rel_path += "../" }
# remove the common path and add the relative path.
path.sub( common_path, rel_path )
end
def include( path )
@files << path
end
def filename( )
OS::Path.set_downcase_on_normalise(false)
OS::Path::combine( @unity_folder, "#{UNITY_PREFIX}#{@project}_#{@name}.cpp" )
end
def write( comment, folder, logues )
File.open(filename(), 'w') do |f|
f.write(to_cpp( comment, folder, logues ))
end
end
def prologue_cpp( )
#"\n\#pragma message( \"Compiling \" __FILE__ \" \" __TIME__)"
""
end
def epilogue_cpp( )
#"\n\#pragma message( \"Compiled \" __FILE__ \" \" __TIME__)"
""
end
def prologue_each_cpp( folder )
file = UnityBuild::PROLOGUE_FILENAME
"\n\#include \"#{file}\""
end
def epilogue_each_cpp( folder )
file = UnityBuild::EPILOGUE_FILENAME
"\n\#include \"#{file}\""
end
def to_cpp( comment, folder, logues )
define = "\n\#define #{UNITY_BUILD_DEFINE}"
includes = ""
@files.each do |file|
includes += "#{prologue_each_cpp(folder) }\n\#include \"#{relative_path(file)}\"#{epilogue_each_cpp(folder)}" if logues
includes += "\n#include \"#{relative_path(file)}\"" unless logues
end
prologue_cpp + define + includes + epilogue_cpp + "\n"
end
def add_regexp( regexp )
#throw ArgumentError.new( "Invalid regexp object specified (#{regexp.class})." ) unless ( regexp.is_a?( Regexp ) )
@regexps << regexp
end
# returns true on first match with regexps
def match( path )
#puts "#{path} matching with #{@regexps.length} regexps"
@regexps.each do |regexp|
regexp = Regexp.new(EMPTY_REGEXP.sub(FOLDER_TOKEN, @name), EMPTY_REGEXP_OPTIONS) if regexp.nil?
#puts "match #{path} #{regexp.to_s}" if path =~ regexp
#puts "no match #{path} #{regexp.to_s}" unless path =~ regexp
#puts "Regexp= #{regexp.to_s} path #{path} #{path =~ regexp}"
return true if path =~ regexp
end
return false
end
end # class UnityFile
###################################################
# The Unity Build System.
#
class UnityBuild
VERSION = "1.0"
REGEXP_COMMENT = /^(\s*)(REM|\#|\/\/)/i
REGEXP_UNITY_FILE = /^(\S*)\s+(\S*)\s+(\S*)/i
REGEXP_EMPTY_UNITY_FILE = /^(\w*)/i
REGEXP_COMMAND = /^(\s*)(set)\s+(\w+)\s*(.*)/i
REGEXP_SPLIT_DELIMITERS = /[\s|\,]/i
UNITY_FOLDER = "_Unity"
LOG_FILE = 'unitybuild'
BUDDY = "\nBuddy: n/a"
COMMAND_ENABLED = "enabled"
COMMAND_EXE = "exe"
COMMAND_EXCLUDE = "exclude"
PROLOGUE_FILENAME = "forceinclude/_unity_prologue.h"
EPILOGUE_FILENAME = "forceinclude/_unity_epilogue.h"
LOGUE_COMMENT = "//Created by #{__FILE__} v#{VERSION} : This file can be edited as required for its project requirements. A default definition is only ever automatically added if it doesn't exist.\n"
attr_accessor :p4 # p4 object - handy since it determines if rage p4 or otherwise!
attr_reader :unity_files # instances of UnityFile class.
attr_reader :other_files # other files in the unity build eg. support files ( _unity_epilogue.h & _unity_prologue.h )
attr_reader :num_excluded # number of excluded files
attr_accessor :logues # logues on or off?
attr_reader :exe # true if the target assembly is an exe.
@@log = nil
def UnityBuild.log
@@log = Log.new( LOG_FILE ) if @@log == nil
@@log
end
def enabled()
# unity_files.length > 0 and @active
@active
end
def initialize( config_file )
begin
@project = OS::Path.get_basename(config_file)
@project_folder = OS::Path.get_directory(config_file)
@unity_folder = OS::Path.combine(@project_folder,UNITY_FOLDER)
@unity_files = []
@other_files = []
@exclusions = []
@num_excluded = 0
@config = Pipeline::Config.instance
@active = true
@logues = true
@exe = false
OS::Path.set_downcase_on_normalise(false)
@config_file = OS::Path.normalise( config_file )
ret = load_config( @config_file )
if ( ret == false )
UnityBuild::log.info " No unity build will be processed due to config file."
@active = false
return self
end
UnityBuild::log.debug ""
UnityBuild::log.debug "Unity Build - Version #{VERSION}"
UnityBuild::log.debug "=========================="
UnityBuild::log.debug " Project folder #{@project_folder}"
UnityBuild::log.debug " Unity folder #{@unity_folder}"
FileUtils.mkdir_p(@unity_folder) unless File.directory?(@unity_folder)
scm_init( )
rescue Exception => ex
puts "Error: Unhandled exception: #{ex.message}"
puts ex.backtrace.join( "\n" )
UnityBuild::log.error( ex, 'Error: Unhandled exception in unitybuild' )
end
end
# load up the unity configuration file
def load_config( path )
begin
OS::Path.set_downcase_on_normalise(false)
@config_file = OS::Path.normalise(path)
if not File.exist? @config_file
UnityBuild::log.warn("The config file #{@config_file} does not exist")
return false
end
UnityBuild::log.debug "0. Parsing unity config file : #{path}..."
count = 1
ok = false
enabled = false
File.open(@config_file, 'r') do |f|
while (line = f.gets)
UnityBuild::log.debug("#{count}: #{line}")
count += 1
line.strip!
is_comment = line =~ REGEXP_COMMENT
if ( (not is_comment) and line.length > 0 )
if (line =~ REGEXP_COMMAND)
command = $3.strip.downcase
params = $4.strip.downcase
UnityBuild::log.debug( " Processing command #{command} #{params}" )
case command
when COMMAND_ENABLED
enabled = true
ok = true
UnityBuild::log.debug( " Enabled" )
when COMMAND_EXE
@exe = true
UnityBuild::log.debug( " Executable" )
when COMMAND_EXCLUDE
@exclusions.concat(params.split(REGEXP_SPLIT_DELIMITERS))
UnityBuild::log.debug( " Exclusions added" )
end
next
end
if ( line !~ REGEXP_UNITY_FILE and line !~ REGEXP_EMPTY_UNITY_FILE)
UnityBuild::log.error("Error: Invalid line in config file #{count}: #{line}")
next
end
name = $1.strip
regexp_str = $2
regexp_options = $3
regexp = nil
if (regexp_str and regexp_options)
regexp = Regexp.new( regexp_str, regexp_options )
regexp_str.strip!
regexp_options.strip!
end
# add to existing unity_file?
added = false
@unity_files.each do |unity_file|
if ( unity_file.name == name )
UnityBuild::log.debug( "Adding another regexp #{name}" )
unity_file.add_regexp( regexp )
added = true
ok = true
break
end
end
if not added
UnityBuild::log.debug( " Adding unity file #{name} #{regexp_str}" )
unity_file = UnityFile.new( name, @unity_folder, @project, @project_folder )
unity_file.add_regexp( regexp )
@unity_files << unity_file
ok = true
end
end
end
end
UnityBuild::log.warn("Unity Build is enabled but not OK") if enabled and not ok
ok and enabled
rescue Exception => ex
puts "Error: Unhandled exception: #{ex.message}"
puts ex.backtrace.join( "\n" )
UnityBuild::log.error( ex, 'Error: Unhandled exception in unitybuild' )
end
end # load_config
# filename is registered with the unity build system
# - returns false if the file is not included in the unity build
# - returns true if the file is included in the unity build
def add_file( path )
begin
OS::Path.set_downcase_on_normalise(false)
OS::Path.normalise( path )
filename = OS::Path.get_filename(path).downcase
# check for specific exclusion.
@exclusions.each do |exclude|
if exclude.downcase==filename
UnityBuild::log.info(" Excluded #{path.downcase}.")
@num_excluded += 1
return false
end
end
matches = 0
@unity_files.each do |unity_file|
if ( unity_file.match( path ) )
unity_file.include( path )
matches += 1
break
end
end
matches==1
rescue Exception => ex
puts "Error: Unhandled exception: #{ex.message}"
puts ex.backtrace.join( "\n" )
UnityBuild::log.error( ex, 'Error: Unhandled exception in unitybuild' )
end
end
# a comment for scm & files.
def comment()
" Automatically generated by unity_build.rb v#{VERSION}\n// Using config in #{@config_file} for project #{@project}\n"
end
# the content of the prologue file ( default )
def prologue_content( )
"#{LOGUE_COMMENT}\#pragma warning(push)"
end
# the content of the epilogue file ( default )
def epilogue_content( )
"#{LOGUE_COMMENT}\#pragma warning(pop)"
end
# create the prologue or epilogue file and update in p4
# the file is only created if it doesn't exist otherwise
# it is left as it is - since there may be custom settings per project.
def create_logue(file, type, content)
filename = OS::Path.combine(@unity_folder, file)
other_files << filename
fstat = @p4.run_fstat( filename ).shift
if (fstat.nil? or fstat['headAction'].include?("delete"))
UnityBuild::log.debug(" Adding a new #{type} #{filename}")
@p4.run_add( filename )
File.open(filename, 'w') do |f|
f.write( content )
end
else
UnityBuild::log.debug(" #{type} #{filename} already exists - it will now be created.")
end
end
# Create unity epilogue and epilogue
# - for want of a better name! - what the f*ck are they collectively?
def create_unity_logues( )
create_logue(PROLOGUE_FILENAME, "prologue", prologue_content)
create_logue(EPILOGUE_FILENAME, "epilogue", epilogue_content)
end
# Writes out new unity files.
def create_unity_files( )
scm_open()
UnityBuild::log.debug(" #{@unity_files.length} unity files.")
total_includes = 0
@unity_files.each do |unity_file|
UnityBuild::log.debug(" Writing (#{unity_file.files.length} includes) in #{unity_file.filename()}" )
unity_file.write( comment(), UNITY_FOLDER, @logues )
total_includes += unity_file.files.length
end
UnityBuild::log.debug(" Creating unity logues ( prologue & epilogue for each file )")
#create_unity_logues() if @logues
UnityBuild::log.debug(" Reverting any files that didn't change")
reverted = []
@unity_files.each do |unity_file|
reverted = @p4.run_revert('-a', unity_file.filename) # revert any files that didn't change
end
UnityBuild::log.debug(" Reverted #{reverted.length} files") if reverted
UnityBuild::log.debug(" #{total_includes} includes written in #{unity_files.length} files ( mean = #{total_includes/(unity_files.length)} includes )" ) if unity_files.length > 0
end
# Create p4 object
def scm_init( )
@p4 = SCM::Perforce::new
@p4.connect()
end
# checkout unity files
def scm_open( )
begin
@p4.connect()
raise Exception if not @p4.connected?
@p4.run_sync( "#{@unity_folder}/*.cpp" )
@unity_files.each do |unity_file|
filename = unity_file.filename()
fstat = @p4.run_fstat( filename ).shift
if (fstat.nil?)
UnityBuild::log.debug " #{filename} added to default CL"
@p4.run_add( filename )
fstat = @p4.run_fstat( filename ).shift
basetype, modifiers = fstat['headType'].split( '+' ) if (fstat.key?('headType')) # perforce drives me dippy.
basetype, modifiers = fstat['type'].split( '+' ) if (fstat.key?('type'))
@p4.run_reopen( '-t', "#{basetype}+w", filename )
else
UnityBuild::log.debug " #{filename} edited in default CL"
basetype, modifiers = fstat['headType'].split( '+' ) if (fstat.key?('headType')) # perforce drives me dippy.
basetype, modifiers = fstat['type'].split( '+' ) if (fstat.key?('type'))
@p4.run_revert( filename )
@p4.run_reopen( '-t', "#{basetype}+w", filename )
@p4.run_sync( filename )
@p4.run_edit( filename )
end
end
rescue Exception => ex
puts "Error: Unhandled exception: #{ex.message}"
puts ex.backtrace.join( "\n" )
UnityBuild::log.error( ex, 'Error: Unhandled exception in unitybuild' )
end
end
end # class UnityBuild
######################################################################################################
# MONKEY PATCHES #####################################################################################
# monkey patch the file class of the projbuilder
class Info::File
attr_accessor :_fullpath_
end
# monkey patch the ProjBuilder class
class ProjBuilder
attr_accessor :projects
# recurse into filters and return array of files
def get_filter_files( filter, filter_path )
files = []
UnityBuild::log.debug " adding #{filter.files.length} files in filter #{filter.path}"
filter.files.each do |file|
UnityBuild::log.debug "filter_path, file.path = #{filter_path}, #{file.path}"
file._fullpath_ = filter_path+file.path
files << file
end
filter.filters.each do |f|
files.concat( get_filter_files( f, filter_path+f.path ) )
end
files
end
# for a project discover all the files within it.
def discover_files( project )
files = []
project.files.each do |file|
UnityBuild::log.debug " adding root file #{file.path}"
file._fullpath_ = file.path
files << file
end
project.filters.each do |filter|
UnityBuild::log.debug " adding root filter #{filter.path}"
filter_files = get_filter_files( filter, filter.path )
filter_files.each do |file|
UnityBuild::log.debug "file discovered : #{file._fullpath_}"
files << file
end
end
return files
end
# Process project builder internal format so it represents a unity build.
def unity_process( unity_config_file, project )
ub = UnityBuild.new(unity_config_file)
if ub.exe
UnityBuild::log.info "Unity build not enabled for executables"
return true if ub.exe # we don't support unity builds for exes yet
end
if not ub.enabled
UnityBuild::log.info "The unity build is not enabled and will not process"
return nil
end
#ub.p4.run_revert(vcproj)
UnityBuild::log.debug "-------------------------------------"
UnityBuild::log.debug "1. Get a list of all the files in the project, record if they have custom build steps or if they are excluded from the build."
files = discover_files( project )
UnityBuild::log.debug " #{files.length} files discovered."
files.each do |file|
UnityBuild::log.debug file.has_custom_build_steps ? " discovered #{file.class} #{file._fullpath_} (custom build steps)" : "discovered #{file.class} #{file._fullpath_}"
file.excluded_from_build = false
end
UnityBuild::log.debug "-------------------------------------"
UnityBuild::log.debug "2. For the list of files in the project add them to the unitybuild."
UnityBuild::log.debug " - unless they have custom build steps."
UnityBuild::log.debug " - unless they are are already excluded from the build."
files_accepted, files_rejected, files_unavailable = [], [], []
files.each do |file|
if ( (not file.has_custom_build_steps) and (not file.excluded_from_build) )
UnityBuild::log.debug "file sent to unity build : #{file._fullpath_}"
accepted = ub.add_file( file.path )
files_accepted << file if accepted
files_rejected << file unless accepted
else
files_unavailable << file
end
end
UnityBuild::log.debug " #{ub.num_excluded} files excluded from unity build"
UnityBuild::log.debug " #{files_unavailable.length} unavailable to unity build"
files_unavailable.each { |file| UnityBuild::log.debug " file unavailable to unity build : #{file._fullpath_}" }
UnityBuild::log.debug " #{files_rejected.length} rejected by unity build"
files_rejected.each { |file| UnityBuild::log.debug " file rejected by unity build : #{file._fullpath_}" }
UnityBuild::log.debug " #{files_accepted.length} accepted by unity build"
files_accepted.each { |file| UnityBuild::log.debug " file accepted by unity build : #{file._fullpath_}" }
UnityBuild::log.info " * Unity build summary : unavailable=#{files_unavailable.length} rejected=#{files_rejected.length} accepted=#{files_accepted.length}."
UnityBuild::log.debug "-------------------------------------"
UnityBuild::log.debug "3. For all files sucessfully added to the unity build now exclude them from the build - for all platforms and build configurations."
files_accepted.each do |file|
UnityBuild::log.debug " file accepted by unity build now excluded from build : #{file._fullpath_}"
file.excluded_from_build = true
end
files = discover_files( project )
excluded = 0
files.each do |file|
if (file.excluded_from_build)
UnityBuild::log.debug " #{file.class} #{file._fullpath_} (verified as excluded from build)"
excluded += 1
end
end
UnityBuild::log.debug " #{excluded} files verified as excluded from build."
UnityBuild::log.debug "-------------------------------------"
UnityBuild::log.debug "4. Create the unity files."
ub.create_unity_files()
UnityBuild::log.debug "-------------------------------------"
UnityBuild::log.debug "5. Add unity files to the project."
if (ub.unity_files.length > 0 and files_accepted.length > 0)
UnityBuild::log.debug " 5.1 create a filter"
filter = Info::Filter.new(UnityBuild::UNITY_FOLDER)
filter.files = []
UnityBuild::log.debug " 5.2 add unity files to the filter"
ub.unity_files.each do |unity_file|
filepath = unity_file.filename
file = Info::File.new( filepath )
filter.files << file
end
#if (ub.logues)
# UnityBuild::log.debug " 5.3 add epilogue and prologue files to the filter"
# ub.other_files.each do |filename|
# file = Info::File.new( filename )
# filter.files << file
# end
#end
end
project.filters << filter
UnityBuild::log.debug "Unity Build Processed OK"
return true
end
end
######################################################################################################
# ENTRY POINT ########################################################################################
# Test - allows the unity build to be tested in _relative isolation_ from the projectbuilder system.
# well it is using the internal format / importer / exporter to test it, but is not injected into the
# project builder.
#
# NB. you should feed it a non unity build project! - unity conversion is not a reverable process - it can't ununitise itself.
if ( __FILE__ == $0 )
g_AppName = File::basename( __FILE__, '.rb' )
OPTIONS = [
[ '--help', '-h', OS::Getopt::BOOLEAN ],
[ '--config', '-c', OS::Getopt::REQUIRED ],
[ '--vcproj', '-p', OS::Getopt::REQUIRED ],
[ '--pretest_projbuilder', '-t', OS::Getopt::BOOLEAN ],
]
opts, trailing = OS::Getopt.getopts( OPTIONS )
if ( opts['help'] )
puts OS::Getopt.usage( OPTIONS )
puts ("Press Enter to continue...")
Process.exit!( 1 )
end
if ( not opts['config'])
$stderr.puts("config not specified in commandline to #{g_AppName}")
puts OS::Getopt.usage( OPTIONS )
Process.exit!( 1 )
end
if ( not opts['vcproj'])
$stderr.puts("vcproj not specified in commandline to #{g_AppName}")
puts OS::Getopt.usage( OPTIONS )
Process.exit!( 1 )
end
pretest_projbuilder = opts['pretest_projbuilder']
vcproj = opts['vcproj']
config_file = opts['config']
format = :vs2005 if vcproj.include?("2005")
format = :vs2008 if vcproj.include?("2008")
format = :vs2010 if vcproj.include?("2010")
UnityBuild::log.debug "unity config #{config_file}"
ub = UnityBuild.new(config_file)
UnityBuild::log.debug "Revert VCproj file"
ub.p4.run_revert(vcproj)
projbuild = ProjBuilder.new( format == :vs2010 )
# Test import / export first - cos I'm about to dick around in the internal representation.
# So I can be sure I haven't broken anything - I need to verify the import/export works first.
if (pretest_projbuilder)
UnityBuild::log.debug "Pre-testing projectbuilder import/export"
if (projbuild.import(vcproj, format))
if ( projbuild.export(vcproj, format) )
UnityBuild::log.debug " project #{vcproj} exported ok"
else
UnityBuild::log.debug " error occurred exporting project before unity build could start! - i.e. not my fault #{vcproj}"
end
end
end
if (projbuild.import(vcproj, format))
projbuild.unity_process(config_file, vcproj.projects[0] )
UnityBuild::log.debug "-------------------------------------"
UnityBuild::log.debug "Export the project."
if ( projbuild.export(vcproj, format) )
UnityBuild::log.debug " #{vcproj} Exported OK."
else
UnityBuild::log.error " error occurred exporting project #{vcproj}"
end
else
UnityBuild::log.error "error occurred importing project #{vcproj}"
end
UnityBuild::log.debug ""
UnityBuild::log.debug "#{__FILE__} Completed OK."
end # if ( __FILE__ == $0 )
end # module ProjBuild
end # module Pipeline