# # File:: %RS_TOOLSLIB%/pipeline/config/project.rb # Description:: Project config.xml wrapper # # Author:: David Muir # Author:: Greg Smith # Date:: June 2008 # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/config/branch' require 'pipeline/config/globals' require 'pipeline/config/targets' require 'pipeline/content/parser' require 'pipeline/os/path' require 'pipeline/util/environment' require 'pipeline/util/float' require 'pipeline/util/time' include Pipeline require 'rexml/document' include REXML #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- module Pipeline # # == Description # # The Project class abstracts a Project project.xml file, providing read-only # access to the XML's attributes including the available project targets and # their platform directory through the ProjectTarget class. # # == Example Usage # # p = Config.instance.project["gta4"] # puts "Project name: #{p.name}" # puts "Project root: #{p.root}" # puts "Project targetdir: #{p.targetdir}" # puts "Number of branches: #{p.branches.size}" # p.branches.each_pair do |name, branch| # puts "Branch: #{name}" # puts "Number of targets: #{branch.targets.size}" # puts "Targets:" # branch.targets.each do |platform, target| # puts "Target: #{target.platform} #{target.target}" # end # end # class Project #--------------------------------------------------------------------- # Attributes #--------------------------------------------------------------------- attr_reader :name # Project name # PATHS attr_reader :sc_proj # Project source control path attr_reader :root # Project root path attr_reader :local # Project local export path attr_reader :cache # Project cache path attr_reader :netstream # Stream Directory attr_reader :netgenstream # Generic stream direcotry (xrefs etc) # PROJECT FLAGS attr_reader :is_episodic # Is the project episodic attr_reader :has_levels # Is the project levelled e.g. Jimmy # BRANCHES attr_reader :branches # Project branches attr_reader :default_branch # Default branch (string key) attr_reader :config # Reference to global config. attr_reader :labels #Project labels # CONFIG LOADED STATUS attr_reader :loaded_config # Project config loaded status attr_reader :loaded_content # Project content loaded status # CONTENT attr_reader :content # Project content tree root # GLOBAL / LOCAL CONFIGURATION attr_accessor :uiname # Project UI-friendly name attr_accessor :root # Project root path # DEFAULT BRANCH attr_reader :audio # Branch audio data root path attr_reader :build # Branch build data root path attr_reader :export # Branch export data path attr_reader :processed # Branch processed data path attr_reader :metadata # Branch metadata data path attr_reader :independent # Branch independent data path (OBSOLETE) attr_reader :common # Branch common data path attr_reader :shaders # Branch shaders path attr_reader :code # Branch code path attr_reader :ragecode # Branch RAGE code path attr_reader :script # Branch script code path attr_reader :art # Branch art path attr_reader :assets # Branch assets path attr_reader :ind_target # Branch independent target object attr_reader :targets # Branch targets Array attr_reader :builders # Branch builders Hash attr_reader :preview # Branch preview directory attr_reader :codeconfigs # Code configurations #--------------------------------------------------------------------- # Utility Classes and Modules #--------------------------------------------------------------------- # # == Description # Class wrapping a single content XML file to be loaded. # class ContentFile attr_reader :name, :filename def initialize( name, filename ) @name = name @filename = filename end end # # == Description # Project XMLParseError exception class. # class XMLParseError < Exception; end #--------------------------------------------------------------------- # Public Methods #--------------------------------------------------------------------- def initialize( name, uiname, root, config, sc_proj ) @name = name @uiname = uiname Pipeline::Globals.instance().in_env do |e| @root = e.subst( root ) @config = e.subst( config ) @sc_proj = e.subst( sc_proj ) end reset() end # # Returns the source control object, making sure it's initialised for # this project. # def scm Config::instance().scm() end # # Returns the source control object, making sure it's initialised for # this project. # def scm_connected Config::instance().scm_connected() end # # Compare this project against another, just uses the name currently # def <=>(otherEntry) ( @name <=> otherEntry.name ) end # # Unload all information for this project # def reset( ) @loaded_config = false @loaded_content = false @targets = {} @branches = {} @default_branch = nil @content = nil @contentfiles = [] @labels = {} end # # Return simple string representation of this object. # def to_s( ) "#{@name}: #{@uiname}" end # # # def to_local_xml( ) root = Element.new( 'local' ) branches_elem = root.add_element( 'branches' ) branches_elem.add_attribute( 'default', @default_branch ) @branches.each_pair do |name, branch| branches_elem.add_element( branch.to_local_xml() ) end root end # # Fill an environment object from this project's state using the # default branch aswell. # def fill_env( env, include_default_branch_env = true ) # Global configuration environment Pipeline::Globals.instance().fill_env( env ) # Import Config instance variables that are of class String. c = Pipeline::Config::instance() c.instance_variables.each do |var| next unless ( c.instance_variable_get( var ).is_a?( String ) ) sym = var.sub( '@', '' ) begin env.lookup( sym ) rescue EnvironmentException # Import into our environment. val = c.instance_variable_get( var ) env.add( sym, val.to_s ) end end # Project environment env.add( 'uiname', env.subst( @uiname ) ) env.add( 'name', env.subst( @name ) ) env.add( 'root', env.subst( @root ) ) env.add( 'content', env.subst( '$(toolsconfig)/content' ) ) env.add( 'cache', env.subst( @cache ) ) # Project default branch environment if ( include_default_branch_env and ( not @default_branch.nil? ) ) then branch = @branches[@default_branch] branch.fill_env( env ) unless ( branch.nil? ) end end # # Run a code block setting up an environment from this object. # def in_env( &block ) env = Environment.new() fill_env( env ) yield( env ) if ( block_given? ) end # # Load in configuration information for this project. # def load_config( force = false ) return true if ( ( @loaded_config ) and ( not force ) ) reset( ) env = Environment.new() env.add( 'root', @root ) env.add( 'filepath', OS::Path.remove_filename(@config) ) @content = Content::Group.new( 'root' ) # Load project global configuration file begin File.open( @config ) do |file| doc = Document.new( file ) @loaded_config = load_from( doc, env, 'project' ) end rescue Exception => ex Project::log().exception( ex, "Unhandled exception loading project config" ) @loaded_config = false end # Load project local configuration file @localconfig = OS::Path.combine( OS::Path.remove_filename(@config), DEFAULT_LOCAL_CONFIG ) return ( @loaded_config ) unless ( File::exists?( @localconfig ) ) begin File.open( @localconfig ) do |file| doc = Document.new( file ) @loaded_config = load_from( doc, env, 'local' ) end rescue Exception => ex Project::log().exception( ex, "Unhandled exception loading project local config" ) end @loaded_config end # # Load project's content tree. # def load_content( branch = nil, force = false, filter = nil ) return ( true ) if ( ( @loaded_content ) and ( not force ) ) throw Exception.new( "can't load content, project not enabled" ) \ unless ( load_config() ) throw Exception.new( "Invalid branch specified: #{branch}." ) \ unless ( branch.nil? or @branches.has_key?( branch ) ) branch = @default_branch if ( branch.nil? ) cp = Content::Parser.new( self, branch ) # Leave existing functionality for the default type (now ALL not DEFAULT) # Pre-pass and store required content. Need to keep content order (output before platform) contentfilelist = [] @content = Content::Group.new( 'root' ) @contentfiles.each do |c| next unless ( File::exists?( c.filename ) ) elapsed_time = Util::time() do newgroup = Content::Group.new( c.name ) newgroup.add_child( cp.parse_xml( c.filename, nil, filter ) ) @content.add_child( newgroup ) end Project::log().info( "Loaded content: #{c.filename} [#{elapsed_time.to_s( 3 )}s]\n" ) end @loaded_content = true @loaded_content end # # Save out any local project information. # def save_locals() return unless ( @loaded_config ) File.open( @localconfig, 'w+' ) do |file| outputXML = REXML::Document.new() outputXML << XMLDecl.new() outputXML << to_local_xml( ) fmt = REXML::Formatters::Pretty.new() fmt.write( outputXML, file ) end end def default_branch=( branch_name ) @default_branch = branch_name initialise_default_branch() end # Return Project class logging object. def Project::log( ) @@log = Log.new( 'project' ) if @@log.nil? @@log end # # Return true iff the passed in filename is within this project's # cache directory structure. # def is_cache_file?( filename ) ( filename.starts_with( cache ) ) end #--------------------------------------------------------------------- # Private Methods #--------------------------------------------------------------------- private @@log = nil # Return the XML attribute value or the project member variable, # XML overridding the project member variable. def get_override_attr_or_member( xml_node, project, member ) return xml_node.attributes[member] unless xml_node.attributes[member].nil? return project.send(member) if project.methods.include?( member ) throw ArgumentError.new( "#{member} not found in XML or project object." ) end # # Parse project data from an XML node, updating member data as # required. # # This allows us to have local.xml overwrite anything in project.xml. # def load_from( xml_node, env, root = 'project' ) env.push( ) Pipeline::Globals.instance().fill_env( env ) env.add( 'root', @root ) @cache = env.subst( get_override_attr_or_member( xml_node.root, self, 'cache' ) ) env.add( 'cache', @cache ) # PROJECT FLAGS @is_episodic = ( 'true' == get_override_attr_or_member( xml_node.root, self, 'is_episodic' ) ) ? true : false @has_levels = ( 'true' == get_override_attr_or_member( xml_node.root, self, 'has_levels' ) ) ? true : false # BRANCHES xml_node.elements.each( "#{root}/branches" ) do |branches| default_name = branches.attributes['default'] unless branches.attributes['default'].nil? @default_branch = default_name unless default_name.nil? end xml_node.elements.each( "#{root}/branches/branch" ) do |branch| branch_name = branch.attributes['name'] throw XMLParseError.new( 'Branch does not specify name. Fix project configuration #{root}.' ) \ if ( branch_name.nil? ) project_branch = nil project_branch = @branches[branch_name] if ( @branches.has_key?( branch_name ) ) # Prevent loading config data for branches in local config that we don't # have a definition for. if ( 'local' == root and project_branch.nil? ) then Project::log().warn( "Not loading local branch '#{branch_name}' as there is no definition for it." ) next end project_branch = Branch::from_xml( branch, env, self, project_branch ) @branches[branch_name] = project_branch end # Initialise default branch, setting our compatibility member data. initialise_default_branch( ) # CONTENT FILES xml_node.elements.each( "#{root}/content/content" ) do |content| throw XMLParseError.new( 'Content files must be defined in a Group node.' ) \ unless ( content.attributes['type'] == 'group' ) throw XMLParseError.new( 'Content files must have a defined name.' ) \ if ( content.attributes['name'].nil? ) name = content.attributes['name'] filename = env.subst( content.children[1].attributes['href'] ) @contentfiles << ContentFile.new( name, filename ) end xml_node.elements.each( "#{root}/labels/label" ) do |label| label_type = label.attributes['type'] name = label.attributes['name'] @labels[label_type] = name end env.pop( ) true end # # Initialise our member data to the default branch (see # @default_branch). # def initialise_default_branch( ) return if ( @default_branch.nil? ) throw XMLParseError.new( "Invalid default branch, key '#{@default_branch}' not found." ) \ unless ( @branches.has_key?( @default_branch ) ) branch = @branches[@default_branch] env = Environment.new() fill_env( env ) env.push( ) branch.fill_env( env ) @audio = env.subst( branch.audio ) @build = env.subst( branch.build ) @export = env.subst( branch.export ) @processed = env.subst( branch.processed ) @metadata = env.subst( branch.metadata ) @independent = env.subst( branch.independent ) @common = env.subst( branch.common ) @shaders = env.subst( branch.shaders ) @code = env.subst( branch.code ) @ragecode = env.subst( branch.ragecode ) @script = env.subst( branch.script ) @art = env.subst( branch.art ) @assets = env.subst( branch.assets ) @preview = env.subst( branch.preview ) @codeconfigs = branch.codeconfigs env.pop( ) # Init targets @ind_target = branch.ind_target @targets = branch.targets @builders = branch.builders end #--------------------------------------------------------------------- # Private Constants #--------------------------------------------------------------------- private DEFAULT_LOCAL_CONFIG = "local.xml" end end # Pipeline module # %RS_TOOLSLIB%/pipeline/config/project.rb