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

502 lines
17 KiB
Ruby
Executable File

#
# File:: %RS_TOOLSLIB%/pipeline/config/project.rb
# Description:: Project config.xml wrapper
#
# Author:: David Muir <david.muir@rockstarnorth.com>
# Author:: Greg Smith <greg@rockstarnorth.com>
# 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