# File:: update_build_state.rb # Description:: updates build state spreadsheet. Interprets a binary, a log file from the game and a version file in order # to populate an existing spreadsheet with this data. # # Author:: Derek Ward # Date:: 07 October 2010 # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'win32ole' require 'pipeline/os/path' require 'time' require 'pipeline/os/getopt' require 'pipeline/os/path' require 'fileutils' include FileUtils require 'dl' include Pipeline require 'pipeline/log/log' require 'util/ExcelTools/excel_tools' #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- OPTIONS = [ [ '--memvis', '-w', Getopt::REQUIRED, 'mem visualize folder' ], [ '--xlsfilename', '-x', Getopt::REQUIRED, 'destination xls filename' ], [ '--datafilename', '-d', Getopt::REQUIRED, 'source filename from which data is extracted for insertion' ], [ '--versionfilename', '-v', Getopt::REQUIRED, 'filename from which version is extracted for insertion' ], [ '--ps3bin', '-p', Getopt::REQUIRED, 'where the ps3bin.exe executable exists' ], [ '--xbox360bin', '-q', Getopt::REQUIRED, 'where the dumpbin.exe executable exists' ], # NB. VS environment needs to be established to run this. [ '--bin', '-b', Getopt::REQUIRED, 'the binary' ], [ '--deltas', '-dt', Getopt::BOOLEAN, 'whacks a line in at the bottom of the spreadsheet that is a delta of the last two results.' ], [ '--disablecheckin', '-c', Getopt::BOOLEAN, 'prevent checkin ( for development )' ] ] INFO = "[colourise=black]INFO_MSG: " INFO_BLUE = "[colourise=blue]INFO_MSG: " # --- keys that appear as output from ps3bin -dsi ( update if ever changes ) KEY_TEXT = "Text (Code)" KEY_DATA = "Data" KEY_RODATA = "RO-Data" KEY_BSS = "BSS (Uninitialised data)" KEY_TOTAL = "Total" KEYS = [ KEY_TEXT, KEY_DATA, KEY_RODATA, KEY_BSS, KEY_TOTAL ] # --- relates respectively to what is found in the XLS spreadsheet BUILDSTATE_STRINGS = [ "Date", "Build", "CL Game", "CL Rage", "Text Size", "Data Size", "RO-Data Size", "BSS Size", "Total", "GH Total", "GH Used", "GH Left", "Default", "Animation", "Streaming", "World", "Gameplay", "FX", "Rendering", "Physics", "Audio", "Network", "System", "Scaleform", "Script", "Resource", "Debug", "Bounds", "Pools", "AI", "Process", "Pathfinding" ] # --- a tag in the version.xml file VERSION_TAG = "[VERSION_NUMBER]" # --- regexps for searching checkin comment of executable REGEXP_CL_GAME = /(.*)gameCL([#|\s|:]*)(\d+)/i REGEXP_CL_RAGE = /(.*)rageCL([#|\s|:]*)(\d+)/i REGEXP_DATE = /(\d+\/\d+\/\d+)/ REGEXP_VERSION = /(\d+\.*\d*)/ #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- # monkey patch the string class class String def convert_base(from, to) self.to_i(from).to_s(to) end end #----------------------------------------------------------------------------- # Code #----------------------------------------------------------------------------- begin #------------------------------------------------------------------------- # Entry-Point #------------------------------------------------------------------------- g_AppName = File::basename( __FILE__, '.rb' ) g_xls_filename = '' g_Config = Pipeline::Config.instance() g_Log = Log.new( 'exceltools' ) #------------------------------------------------------------------------- # Parse & validate Command Line #------------------------------------------------------------------------- opts, trailing = OS::Getopt.getopts( OPTIONS ) if ( opts['help'] ) then puts OS::Getopt.usage( OPTIONS ) response = message_box( "#{g_AppName} will exit.",g_AppName, BUTTONS_OK, ICON_QUESTION) exit( 1 ) end g_mem_vis = ( nil == opts['memvis'] ) ? nil : opts['memvis'] g_xls_filename = ( nil == opts['xlsfilename'] ) ? nil : opts['xlsfilename'] g_data_filename = ( nil == opts['datafilename'] ) ? nil : opts['datafilename'] g_version_filename = ( nil == opts['versionfilename'] ) ? nil : opts['versionfilename'] g_ps3bin_filename = ( nil == opts['ps3bin'] ) ? nil : opts['ps3bin'] g_xbox360bin_filename = ( nil == opts['xbox360bin'] ) ? nil : opts['xbox360bin'] g_bin_filename = ( nil == opts['bin'] ) ? nil : opts['bin'] g_enable_checkin = ( nil == opts['disablecheckin'] ) ? true : false g_deltas = ( nil == opts['deltas'] ) ? false : true if g_data_filename and not File.exist? g_data_filename $stderr.puts "Error : #{g_data_filename} does not exist." exit( 2 ) end if g_version_filename and not File.exist? g_version_filename $stderr.puts "Error : #{g_version_filename} does not exist." exit( 3 ) end if g_ps3bin_filename and not File.exist? g_ps3bin_filename $stderr.puts "Error : #{g_ps3bin_filename} does not exist." exit( 4 ) end if g_xbox360bin_filename and not File.exist? g_xbox360bin_filename $stderr.puts "Error : #{g_xbox360bin_filename} does not exist." exit( 6 ) end if g_bin_filename and not File.exist? g_bin_filename $stderr.puts "Error : #{g_bin_filename} does not exist." exit( 5 ) end #------------------------------------------------------------------------- # --- Sync, check out file --- #------------------------------------------------------------------------- puts "#{INFO_BLUE} Creating p4" p4 = SCM::Perforce::create( g_Config.sc_server, g_Config.sc_username, g_Config.sc_workspace ) p4_rage = SCM::Perforce::create( g_Config.sc_rage_server, g_Config.sc_rage_username, g_Config.sc_rage_workspace ) puts "#{INFO_BLUE} Connecting to p4 servers" raise Exception if not p4 p4.connect( ) raise Exception if not p4.connected? raise Exception if not p4_rage p4_rage.connect( ) raise Exception if not p4_rage.connected? #Why did I do this - this is incorrect - keeping it around until I think if there was a good reason for this -- # maybe I thought it wasn;t part of the label - but it is in the label. # if (g_version_filename) # puts "#{INFO_BLUE} Syncing to version file." # depot_filename = p4.local2depot(g_version_filename) # p4.run_sync( g_version_filename ) # end if (g_ps3bin_filename) puts "#{INFO_BLUE} Syncing to ps3bin file." depot_filename = p4_rage.local2depot( g_ps3bin_filename ) p4_rage.run_sync( g_ps3bin_filename ) end #------------------------------------------------------------------------- # Derive CLs used to build executable from it's checkin comment. #------------------------------------------------------------------------- if (g_bin_filename) depot_filename = p4.local2depot( g_bin_filename ) puts "#{INFO_BLUE} EXE= #{depot_filename}" change = p4.run_changes( "-m1", "-l", "#{depot_filename}@#{p4.client}" ) puts "#{INFO_BLUE} EXE CL= #{change[0]['change']}" description = change[0]['desc'] if (description) puts "#{INFO_BLUE} EXE descripton= #{description}" trailing[2] = $3 if (trailing[2].nil? and description =~ REGEXP_CL_GAME) if trailing[2] puts "#{INFO_BLUE} EXE Built with CL #{trailing[2]} from game p4 server" else puts "Warning: The CL (game P4) could not be derived from the checkin comment #{description}" trailing[2] = "unknown" #puts "Warning: No update will be done." #exit( 0 ) end trailing[3] = $3 if (trailing[3].nil? and description =~ REGEXP_CL_RAGE) if trailing[3] puts "#{INFO_BLUE} EXE Built with CL #{trailing[3]} from rage p4 server" else puts "Warning: The CL (rage p4) could not be derived from the checkin comment #{description}" trailing[3] = "unknown" #puts "Warning: No update will be done." #exit( 0 ) end end end puts "#{INFO_BLUE} Creating CL" change_id = p4.create_changelist( "Automated Build of 'build state' via Cruise Control @ #{Time.now} on #{ENV["COMPUTERNAME"]}." ) raise Exception if change_id.nil? puts "#{INFO_BLUE}Changelist #{change_id} is created" puts "#{INFO_BLUE} Syncing to memory XLS file." depot_filename = p4.local2depot(g_xls_filename) p4.run_sync( depot_filename ) puts "#{INFO_BLUE}Checking out #{depot_filename} in CL #{change_id.to_s}" puts "Checking out #{depot_filename} in CL #{change_id.to_s}" p4.run_edit( '-c', change_id.to_s, depot_filename ) p4.run_reopen( '-c', change_id.to_s, depot_filename ) p4.run_edit( '-c', change_id.to_s, g_mem_vis ) p4.run_reopen( '-c', change_id.to_s, g_mem_vis ) #------------------------------------------------------------------------- # --- Append the row --- #------------------------------------------------------------------------- puts "AppendRow to #{g_xls_filename}" if (g_data_filename) #------------------------------------------------------------------------- # --- Search for data in log. #------------------------------------------------------------------------- buildstate_regexp = [ ] puts "#{INFO_BLUE} building regexps" buildstate_strings = BUILDSTATE_STRINGS buildstate_strings.each do |str| buildstate_regexp << Regexp.new("(.*)BuildState(.*)#{str}=(.*)$") end buildstate_regexp.each do |regexp| puts "#{INFO_BLUE} Regexp #{regexp.to_s}" end puts "#{INFO_BLUE} opening #{g_data_filename}" File.open(g_data_filename) do |file| file.each_line do |line| buildstate_regexp.each_with_index do |regexp, idx| if line =~ regexp puts "#{INFO_BLUE} Matched #{line}" trailing[idx] = $3 end end end end #------------------------------------------------------------------------- #--- Derive missing data not found in log, starting here with time/date and #--- version. #------------------------------------------------------------------------- trailing[0] = Time.now.strftime("%Y/%m/%d") if trailing[0].nil? if trailing[1].nil? puts "#{INFO_BLUE} Opening #{g_version_filename}" File.open(g_version_filename) do |file| capture = false file.each_line do |line| line.gsub!(/[\n]+/, ""); if capture puts "#{INFO_BLUE} Version read #{line}" trailing[1] = "dev build #{line} (automated)" break end capture = true if line.include?VERSION_TAG end end end #------------------------------------------------------------------------- # Derive executable memory segmentation sizes from executable. #------------------------------------------------------------------------- error_count = 0 if ( trailing[4].nil? or trailing[5].nil? or trailing[6].nil? or trailing[7].nil? or trailing[8].nil?) if (g_ps3bin_filename) cmd = "#{g_ps3bin_filename} #{g_bin_filename} -dsi" puts "#{INFO_BLUE} Executing #{cmd}...\n" status, stdout, stderr = systemu(cmd) puts "#{INFO_BLUE} \n...Completed" $stderr.puts( cmd ) if ( stderr.length > 0 ) stderr.each do |err| error_count += 1 $stderr.puts "Error: #{err}" end stdout.each_with_index do |out, idx| puts out if (idx==1) vals = out.split hash = {} KEYS.each_with_index do |key,idx| puts "#{INFO_BLUE} Bin stats : #{key} = #{vals[idx]}" hash[key] = vals[idx] end trailing[4] = hash[KEY_TEXT] trailing[5] = hash[KEY_DATA] trailing[6] = hash[KEY_RODATA] trailing[7] = hash[KEY_BSS] trailing[8] = hash[KEY_TOTAL] end end elsif (g_xbox360bin_filename) cmd = "#{g_xbox360bin_filename} #{g_bin_filename} /Summary" puts "#{INFO_BLUE} Executing #{cmd}...\n" status, stdout, stderr = systemu(cmd) puts "#{INFO_BLUE} \n...Completed" $stderr.puts( cmd ) if ( stderr.length > 0 ) stderr.each do |err| error_count += 1 $stderr.puts "Error: #{err}" end total = 0 stdout.each_with_index do |out, idx| if out =~ /^\s*([0-9A-F]*)\s*\.(.*)$/i hex_size = $1 section = $2 size = hex_size.convert_base(16,10).to_i if (section=="text") trailing[4] = size puts "Section #{section} is #{size}" elsif (section=="data") trailing[5] = size puts "Section #{section} is #{size}" elsif (section=="rdata") trailing[6] = size puts "Section #{section} is #{size}" else puts "Warning: section #{section} #{size} is ignored" end # DW: NB. only text & data are read here, as they are really the only sections that make much sense to the end user in the final report # anybody interested or knowledgable about these sections though can analyse by hand. # The XEX signing process hopefully will take care of many optimisations to such sections.?! #rdata section #In MS Windows PE file terms, the .rdata section is used for at least four things: #- First, in EXEs produced by Microsoft Link, the .rdata section holds the debug directory; #- Second, the description string - intended to hold a useful text string describing the file; #- Third, describe GUIDs objects used in OLE programming. #- Forth, place to put the TLS (Thread Local Storage) directory. #Pdata Section #The .pdata section contains an array of function table entries used for exception handling and is pointed to by the exception table entry in the image data directory. #reloc section #In MS Windows PE file terms, the .reloc section holds a table of base relocations - a list of places in the image where the difference between the linker-assumed load address and the actual load address needs to be taken into account. #A base relocation is an adjustment to an instruction or initialized variable value (locations of static variables, string literals and so on). #The relocation directory is a sequence of chunks where each chunk contains the relocation information for 4 KB of the image. #If the loader can load the image at the linker's preferred base address, the loader ignores the relocation information in this section. #trailing[7] = hash[KEY_BSS] total += size else puts "skipped #{out}" end end trailing[8] = total puts "Total is #{total}" end end buildstate_strings.each_with_index do |str, idx| trailing[idx] = "0" if trailing[idx].nil? puts "#{INFO_BLUE} #{str} #{trailing[idx]}" end end #------------------------------------------------------------------------- # -- Finally append this row to the spreadsheet & compute a delta. #------------------------------------------------------------------------- last_row = ExcelTools::Row(g_xls_filename, -1) puts "#{INFO_BLUE} last row was #{last_row.join(" ")}" if (last_row[0] =~ /DELTA/i ) puts "#{INFO_BLUE} Delta found, delete delta row" ExcelTools::DeleteRow(g_xls_filename, -1) end # DW: It has been noticed that the delete row above sometimes doesn't delete the row. # Not sure why, perhaps saving the document on another thread, but it;s not the time or place to rewrite this, so... 2.times do Kernel.sleep(5) last_row = ExcelTools::Row(g_xls_filename, -1) puts "#{INFO_BLUE} last row was #{last_row.join(" ")}" if (last_row[0] =~ /DELTA/i ) puts "#{INFO_BLUE} Error : Delta found, delete delta row, this should not happen, I'll delete it anyway" ExcelTools::DeleteRow(g_xls_filename, -1) else break end end #now replace the entry if it is the same version on the same day last_row = ExcelTools::Row(g_xls_filename, -1) puts "#{INFO_BLUE} Previous row was #{last_row.join(" ")}" if (last_row[0] =~ /DELTA/i ) puts "#{INFO_BLUE} Error : A delta row has persisted in the xls document, depsite attempts to remove it. This document will need to be hand edited. (#{g_xls_filename})" end last_date = last_row[0] =~ REGEXP_DATE ? $1 : nil this_date = trailing[0] =~ REGEXP_DATE ? $1 : nil puts "#{INFO_BLUE}last_date #{last_date}" puts "#{INFO_BLUE}this_date #{this_date}" if (last_date and this_date and (last_date == this_date) ) puts "#{INFO_BLUE}Last row was of the same date" #strip out version numbers - comments are not important num1 = last_row[1] =~ REGEXP_VERSION ? $1 : nil num2 = trailing[1] =~ REGEXP_VERSION ? $1 : nil if (num1 and num2 and (num1 == num2) ) puts "#{INFO_BLUE} Deleting previous row since it is of same date #{last_row[0]} and version #{last_row[1]}" ExcelTools::DeleteRow(g_xls_filename, -1) end end puts "#{INFO_BLUE}Appending row" ExcelTools::AppendRow(g_xls_filename, trailing) if (g_deltas) begin puts "#{INFO_BLUE} Delta is being computed" last_rows = [ ExcelTools::Row(g_xls_filename, -1), ExcelTools::Row(g_xls_filename, -2) ] deltas = [ "DELTA", "", "", "" ] length_to_skip = deltas.length - 1 last_rows[0].each_with_index do |num, idx| if (idx > length_to_skip) delta = num-last_rows[1][idx] deltas << delta end end puts "#{INFO_BLUE} Delta is #{deltas.join(" ")}" ExcelTools::AppendRow(g_xls_filename, deltas) rescue Exception => ex $stderr.puts "Error : #{g_AppName} unhandled exception: #{ex.message}" $stderr.puts "Call stack:" $stderr.puts ex.backtrace.join( "\n\t" ) end end #------------------------------------------------------------------------- # --- Submit changes #------------------------------------------------------------------------- puts "#{INFO_BLUE}Reverting unchanged files #{change_id}" p4.run_revert( '-a', '-c', change_id.to_s, '//...') files = p4.run_opened( '-c', change_id.to_s ) raise Exception if files.nil? puts "#{INFO_BLUE}There are #{files.size} files to submit." files.each do |file| puts "#{INFO_BLUE}#{file['depotFile']} has been updated and will be submitted." end if ( g_enable_checkin ) if ( files.size > 0 ) puts "#{INFO_BLUE}Submitting file currently in #{change_id}" submit_result = p4.run_submit( '-c', change_id.to_s ) puts submit_result.to_s elsif ( 0 == files.size ) puts "#{INFO_BLUE}Deleting #{change_id} no files changed." p4.run_change('-d', change_id.to_s) end else puts "#{INFO_BLUE}Checkin is disabled the CL is pending." end rescue Exception => ex puts "#{g_AppName} unhandled exception: #{ex.message}" puts "Call stack:" puts ex.backtrace.join( "\n\t" ) end # Exceltools.rb