# # File:: cc_make.rb # Description:: processes simple xml files to produce various cruise control configuration files. # # https://devstar.rockstargames.com/wiki/index.php?title=Tool_Builder&action=edit&redlink=1 # # Author:: Derek Ward # Date:: 20th June 2011 # # Passed in :- see OPTIONS ... # Passed out :- # # Returns :- #----------------------------------------------------------------------------- # Uses / Requires #----------------------------------------------------------------------------- require 'pipeline/os/path' require 'pipeline/os/getopt' require 'pipeline/log/log' require 'xml' require "rexml/document" include REXML include Pipeline #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- INFO_BLUE = "[colourise=blue]INFO_MSG: " HELP_URL = "https://devstar.rockstargames.com/wiki/index.php?title=Tool_Builder&action=edit&redlink=1" FUNCTION = "toolbuilder" # # solution class - spec for build class Solution NAMESPACE = "urn:ccnet.config.builder" DEFAULT_INCREDICMD = "/build" PS3_INCREDICMD = "/incredi" DEFAULT_REBUILD_INCREDICMD = "/rebuild" PS3_REBUILD_INCREDICMD = "/incredi /rebuild" CB_SCOPE = 'cb:scope' CB_NAMESPACE = 'xmlns:cb' CB_INCLUDE = "cb:include" SPLIT_CONFIGS = "," SPLIT_ENV = "," DEFAULT_TASK_EXEC_FILE = "cruisecontrol/#{FUNCTION}/#{FUNCTION}_task_incredibuild.xml" PS3_TASK_EXEC_FILE = "cruisecontrol/#{FUNCTION}/#{FUNCTION}_task_vsi.xml" PS3_BUILD_IDENTIFIER = "ps" MAX_CUSTOM_ENV_VARIABLES = 4 attr_accessor :name, :platform_compiler, :build_config, :sln, :email_breakers, :owner, :extra_targets, :extra_slns, :config, :fold, :env def initialize(node) @name = node["name"] @config = node["config"] @sln = node["sln"] @owner = node["owner"] @email_breakers = node["email_breakers"] @env = node["env"] @rebuild = node["rebuild"] if @rebuild.nil? @rebuild = false else @rebuild = true if (@rebuild.downcase != "false" and @rebuild.downcase != "0") end if @email_breakers.nil? @email_breakers = false else @email_breakers = true if (@email_breakers.downcase != "false" and @email_breakers.downcase != "0") end @extra_targets = [] @extra_slns = [] @fold = -1 get_platform_and_config_names() end def to_s() email_breakers = @email_breakers == true ? "email breakers=on" : "email breakers=off" rebuild = @rebuild == true ? "rebuild=true" : "rebuild=false" str = "#{@name.ljust(20)}owner= #{@owner.ljust(30)}#{email_breakers.to_s.ljust(20)}#{rebuild.to_s.ljust(15)}\n#{INFO_BLUE}\t\t#{@sln.ljust(80)}#{@config.ljust(20)}" @extra_targets.each_with_index do |t,i| str += "\n#{INFO_BLUE}\t\t#{@extra_slns[i].ljust(80)}#{t.ljust(20)}" end str end def can_fold( solution ) ( @name == solution.name and @email_breakers == solution.email_breakers and @owner == solution.owner) end # # Split up the config string and determine what to call this project def get_platform_and_config_names() platforms, configs = [],[] config_split = @config.split(SPLIT_CONFIGS) config_split.each do |config| split_config = config.split("|") if ( split_config.length > 1 ) platforms << split_config[0] configs << split_config[1] end end platforms.uniq! configs.uniq! @platform_compiler = platforms.length > 1 ? platforms.length.to_s : platforms[0] @build_config = configs.length > 1 ? configs.length.to_s : configs[0] end # # convert solution to xmldoc suitable for CC use. def to_xml( ) xmldoc = XML::Document.new xmldoc.encoding = XML::Encoding::UTF_8 xmldoc.root = XML::Node.new( CB_SCOPE ) xmldoc.root.attributes[CB_NAMESPACE] = NAMESPACE xmldoc.root.attributes['project'] = @name xmldoc.root.attributes['platform_compiler'] = @platform_compiler xmldoc.root.attributes['build_config'] = @build_config xmldoc.root.attributes['toolbuild_owner_email'] = @owner xmldoc.root.attributes['standard_email'] = "" unless @email_breakers == true if @rebuild == true xmldoc.root.attributes['incredicmd'] = @config.downcase.include?(PS3_BUILD_IDENTIFIER) ? PS3_REBUILD_INCREDICMD : DEFAULT_REBUILD_INCREDICMD else xmldoc.root.attributes['incredicmd'] = @config.downcase.include?(PS3_BUILD_IDENTIFIER) ? PS3_INCREDICMD : DEFAULT_INCREDICMD end xmldoc.root.attributes['task_exec_file'] = @config.downcase.include?(PS3_BUILD_IDENTIFIER) ? OS::Path.combine(ENV["RS_TOOLSCONFIG"],PS3_TASK_EXEC_FILE) : OS::Path.combine(ENV["RS_TOOLSCONFIG"],DEFAULT_TASK_EXEC_FILE) xmldoc.root.attributes['targets'] = @config xmldoc.root.attributes['solution_file'] = @sln if (@env) env_split = @env.split(SPLIT_ENV) puts "Warning: Cruise control config needs expended to cater for #{env_split.length} ENV variables" if (env_split.length > MAX_CUSTOM_ENV_VARIABLES) env_split.each_with_index do |e,idx| name_val = e.split("=") if (name_val.length==2) env_name = name_val[0].strip env_val = name_val[1].strip xmldoc.root.attributes["custom_env_variable_name_#{idx+1}"] = env_name xmldoc.root.attributes["custom_env_variable_value_#{idx+1}"] = env_val end end end # the solution can have extra configs and solutions it targets ( these are coupled - but linear in the output xml ) @extra_targets.each_with_index do |target,idx| xmldoc.root.attributes["targets_#{idx+1}"] = target task_exec_file,incredicmd = nil, nil if (target.downcase.include?(PS3_BUILD_IDENTIFIER)) task_exec_file = OS::Path.combine(ENV["RS_TOOLSCONFIG"],PS3_TASK_EXEC_FILE) incredicmd = PS3_REBUILD_INCREDICMD if @rebuild incredicmd = PS3_INCREDICMD unless @rebuild else task_exec_file = OS::Path.combine(ENV["RS_TOOLSCONFIG"],DEFAULT_TASK_EXEC_FILE) incredicmd = DEFAULT_REBUILD_INCREDICMD if @rebuild incredicmd = DEFAULT_INCREDICMD unless @rebuild end xmldoc.root.attributes["incredicmd_#{idx+1}"] = incredicmd xmldoc.root.attributes["task_exec_file_#{idx+1}"] = task_exec_file end @extra_slns.each_with_index do |sln,idx| xmldoc.root.attributes["solution_file_#{idx+1}"] = sln end include = XML::Node.new(CB_INCLUDE) include.attributes['href'] = "$(base_file)" xmldoc.root << include xmldoc end end # CC make class CCMaker DEFAULT_BRANCH = "dev" REGEXP_BRANCH = /^.*\\(\w*)/i MACHINE = ENV["COMPUTERNAME"] # "EDIW-CC-8" if run remotely. REMOTING_PORT = "21234" REMOTING_FILE = "CruiseManager.rem" SERVER_URL = "tcp://#{MACHINE}:#{REMOTING_PORT}/#{REMOTING_FILE}" CCTRAY_PROJECTS_XPATH = "Configuration/Projects" CCTRAY_ALL_PROJECTS_XPATH = 'Configuration/Projects/Project' SOLUTIONS_XPATH = "//solutions/solution" KEEP_CCTRAY_PROJECT_REGEXP = /^(toolbuilder_maker)/ IB_SUPPORTS_MULTIPLE_CONFIGS = false ENV_SYMBOLS = [ "RAGE_DIR", "RS_TOOLSSRC", "RS_TOOLSCONFIG", "RS_CODEBRANCH", "RAGE_3RDPARTY", "RAGE_3RDPARTY", "RS_PROJECT" ] MAX_COMBINED_CONFIGS = 4 # if update this you will need to update the cruise control stub also see def initialize() @config = Pipeline::Config.instance @config.project.load_config @env = Environment.new() @config.project.fill_env( @env ) ENV_SYMBOLS.each { |sym| @env.add(sym,ENV[sym]) } @p4 = SCM::Perforce.new() @p4.port = @config.sc_server @p4.client = @config.sc_workspace @p4.user = @config.sc_username @p4.connect() @solutions = [] end # # main control process def process(filenames, targetDir, cctray_file, submit ) make( filenames ) return false unless solutions_exist() save( targetDir ) update_cctray( cctray_file ) submit_or_revert(submit) true end # # create within the class an xml representation of the history. def make( filenames ) filenames.each do |filename| xmldoc = LibXML::XML::Document::file( filename ) nodes = xmldoc.find(SOLUTIONS_XPATH) # unfortuinately IB wont support mutiple build configs to build at the same time - this would be unnecessary if it could nodes.each do |node| if (IB_SUPPORTS_MULTIPLE_CONFIGS) @solutions << Solution.new(node) else config_split = node['config'].split(Solution::SPLIT_CONFIGS) config_split.each do |config| node['config'] = config @solutions << Solution.new(node) end end end end fold_solutions() end # # based upon some rules various solutions are combined in a bigger 'mega' ( within limits ) solution. def fold_solutions() # one day I'll be able to do this all in one line of ruby - but not on a Friday. @solutions.each_with_index do |solution,i| @solutions.each_with_index do |solution2,i2| solution2.fold = i if (i2 > i and solution2.fold < 0 and solution2.can_fold(solution) ) end end @solutions.each_with_index do |solution,i| if solution.fold > -1 @solutions[solution.fold].extra_targets << solution.config @solutions[solution.fold].extra_slns << solution.sln throw RuntimeError.new( "MAX_COMBINED_CONFIGS is #{MAX_COMBINED_CONFIGS} for solution #{solution.to_s}") if @solutions[solution.fold].extra_targets.length > MAX_COMBINED_CONFIGS-1 end end # rename them @solutions.each do |solution| if (solution.extra_targets.length > 0 or solution.extra_slns.length > 0 ) solution.platform_compiler = (solution.extra_targets.length+1).to_s solution.build_config = "targets" end end @solutions.delete_if { |solution| solution.fold >= 0 } puts "#{INFO_BLUE} Folded Solutions comprise..." @solutions.each { |solution| puts ("#{INFO_BLUE}\t#{full_project_name(solution).ljust(80)}\t#{solution.to_s}") } end # # determine which solutions exist def solutions_exist() all_exist = true @solutions.each do |solution| filename = @env.subst( solution.sln ) filename = File::expand_path( filename ) if (not File.exists? filename) $stderr.puts("Error: Solution filename #{filename} not found.") all_exist = false end end all_exist end # # purge all old target files def purge(target_dir) # Purge dir @p4.run_sync(target_dir) @p4.run_delete('-c', @change_id.to_s, OS::Path.combine(target_dir,"#{FUNCTION}_target_*.xml") ) end # # save files def save( target_dir ) #FileUtils::mkdir_p( OS::Path::get_directory( filename ) ) unless ( File::directory?( filename ) ) @change_id = @p4.create_changelist( "Automatically generated by cc_make.rb" ) purge(target_dir) @solutions.each do |solution| xmldoc = solution.to_xml( ) dest_file = OS::Path.combine(target_dir, "#{FUNCTION}_target_#{unique_project_name(solution)}.xml") #puts "#{INFO_BLUE} Writing #{dest_file}" @p4.run_sync( dest_file ) @p4.run_revert( '-c', @change_id.to_s, dest_file) @p4.run_edit_or_add( '-c', @change_id.to_s, dest_file ) @p4.run_reopen( '-c', @change_id.to_s, dest_file ) xmldoc.save( dest_file ) end save_target_list( target_dir ) end # submit or revert files def submit_or_revert( submit ) @p4.run_revert( '-a', '-c', @change_id.to_s, '//...') files = @p4.run_opened( '-c', @change_id.to_s ) if ( files.size > 0 ) if (submit) puts "#{INFO_BLUE} Submitting #{files.size} files in CL #{@change_id}" puts "Warning: %RS_TOOLSROOT%/script/util/CruiseControl/CCTray/cctray_toolsbuilder.bat should be run to update your cctray." files.each {|file| puts "#{INFO_BLUE} #{file['depotFile']}" } submit_result = @p4.run_submit( '-c', @change_id.to_s ) else puts "#{INFO_BLUE} Pending #{files.size} files in CL #{@change_id}" end elsif ( 0 == files.size ) puts "#{INFO_BLUE} No files changed - CL #{@change_id} deleted" @p4.run_change('-d', @change_id.to_s) end end # # Get the full project name eg. toolbuilder_gta5_dev_ragebuilder_win32_release def full_project_name(solution) project_name = ENV['RS_PROJECT'].downcase branch_name = DEFAULT_BRANCH branch_name = $1.downcase if (ENV['RS_CODEBRANCH'] =~ REGEXP_BRANCH) branch_name = DEFAULT_BRANCH if branch_name.length == 0 "#{FUNCTION}_#{project_name}_#{branch_name}_#{solution.name}_#{solution.platform_compiler}_#{solution.build_config}" end # # Get the unique project name withoput the faff as a prefix eg. ragebuilder_win32_release NOT this toolbuilder_gta5_dev_ragebuilder_win32_release def unique_project_name(solution) "#{solution.name}_#{solution.platform_compiler}_#{solution.build_config}" end # # Update the CCTray config file def update_cctray( cctray_file ) @p4.run_sync( cctray_file ) fstat = @p4.run_fstat( cctray_file ).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_edit_or_add( '-c', @change_id.to_s, cctray_file ) @p4.run_reopen( '-c', @change_id, cctray_file ) @p4.run_reopen( '-t', "#{basetype}+w", '-c', @change_id.to_s, cctray_file ) xmldoc = REXML::Document.new File.open( cctray_file, 'r' ) # new working element store new_projects_elem = Element.new( "Projects" ) # keep some existing elements xmldoc.elements.each( CCTRAY_ALL_PROJECTS_XPATH ) do |elem| if elem.attributes["projectName"] =~ KEEP_CCTRAY_PROJECT_REGEXP #puts "#{INFO_BLUE} Keeping CCtray project #{elem.attributes["projectName"]}" new_projects_elem.add_element(elem.clone) end end # add all elements as defined by solutions @solutions.each do |solution| found = false project_name = full_project_name(solution) #puts "#{INFO_BLUE} Adding CCtray project #{project_name}" elem = Element.new( "Project" ) elem.add_attribute("serverUrl", SERVER_URL) elem.add_attribute("projectName", project_name) elem.add_attribute("showProject", "true") new_projects_elem.add_element(elem) end # switch to the new element in the xmldoc xmldoc.delete_element(CCTRAY_PROJECTS_XPATH) xmldoc.root.add_element(new_projects_elem) File.open( cctray_file, 'w' ) do |file| fmt = REXML::Formatters::Default.new() fmt.write(xmldoc,file) end end private # # Save a list of targets def save_target_list( target_dir ) xmldoc = target_list_to_xml( ) dest_file = OS::Path.combine(target_dir, "#{FUNCTION}_targets.xml") #puts "#{INFO_BLUE} Writing #{dest_file}" @p4.run_sync( dest_file ) @p4.run_edit_or_add( '-c', @change_id.to_s, dest_file ) @p4.run_reopen( '-c', @change_id.to_s, dest_file ) xmldoc.save( dest_file ) end # # Get xml doc for target file def target_list_to_xml( ) xmldoc = XML::Document.new xmldoc.encoding = XML::Encoding::UTF_8 xmldoc.root = XML::Node.new( Solution::CB_SCOPE ) xmldoc.root.attributes['xmlns:cb'] = Solution::NAMESPACE @solutions.each do |solution| include = XML::Node.new(Solution::CB_INCLUDE ) include.attributes['href'] = "$(tools_build_dir)\\#{FUNCTION}_target_#{unique_project_name(solution)}.xml" xmldoc.root << include end xmldoc end # log def self.log( ) @@log = Log.new( 'cc_make' ) if ( @@log.nil? ) @@log end @@log = nil end #---------------------------------------------------------------------------- # Implementation #---------------------------------------------------------------------------- if ( __FILE__ == $0 ) then OPTIONS = [ [ "--help", "-h", OS::Getopt::BOOLEAN, "display usage information." ], [ '--target_dir', '-t', OS::Getopt::REQUIRED, "directory to output results to" ], [ '--cctray' , '-c', OS::Getopt::OPTIONAL, "cctray file to update" ], [ '--submit' , '-s', OS::Getopt::BOOLEAN, "submit or revert" ], ] begin g_AppName = File::basename( __FILE__, '.rb' ) #--------------------------------------------------------------------- # Parse Command Line #--------------------------------------------------------------------- opts, g_Trailing = OS::Getopt.getopts( OPTIONS ) if ( opts['help'] ) then puts OS::Getopt.usage( OPTIONS ) exit( 1 ) end if ( not opts['target_dir'] ) then puts OS::Getopt.usage( OPTIONS ) exit( 1 ) end if (g_Trailing.length <= 0) puts OS::Getopt.usage( OPTIONS ) puts "Supply files to read" exit( 1 ) end g_TargetDir = opts['target_dir'] g_CCtrayFile = opts['cctray'] g_Submit = opts["submit"] if (g_CCtrayFile) filename = File::expand_path( g_CCtrayFile ) if (File.exist? filename) g_CCtrayFile = filename else g_CCtrayFile = nil $stderr.puts("Error: filename #{filename} not found") end end g_Filenames = [] Pipeline::OS::Path::set_downcase_on_normalise( false ) g_Trailing.each_with_index do |filename, index| filename = File::expand_path( filename ) if (File.exist? filename) g_Filenames << filename else $stderr.puts("Error: filename #{filename} not found") end end Pipeline::OS::Path::set_downcase_on_normalise( true ) # Make puts "#{INFO_BLUE} #{HELP_URL}" cc_maker = CCMaker.new() ret = cc_maker.process( g_Filenames, g_TargetDir, g_CCtrayFile, g_Submit ) Process.exit! -2 unless ret rescue Exception => ex $stderr.puts "Error: Unhandled exception: #{ex.message}" puts ex.backtrace().join("\n") Process.exit! -1 end Process.exit! 0 end # %RS_TOOLSLIB%/util/CruiseControl/cc_make.rb