Skip to content

Commit

Permalink
Refactor Pod::Xcode::Project to use actual objects instead of raw has…
Browse files Browse the repository at this point in the history
…hes.
  • Loading branch information
alloy committed Oct 11, 2011
1 parent 7c8ac07 commit ffefba9
Show file tree
Hide file tree
Showing 2 changed files with 355 additions and 138 deletions.
359 changes: 251 additions & 108 deletions lib/cocoapods/xcode/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,212 @@
module Pod
module Xcode
class Project
class PBXObject
def self.attributes_accessor(*names)
names.each do |name|
name = name.to_s
define_method(name) { @attributes[name] }
define_method("#{name}=") { |value| @attributes[name] = value }
end
end

def self.isa
@isa ||= name.split('::').last
end

attr_reader :uuid, :attributes
attributes_accessor :isa, :name

def initialize(project, uuid, attributes)
@project, @uuid, @attributes = project, uuid || generate_uuid, attributes
self.isa ||= self.class.isa
end

def inspect
"#<#{isa} UUID: `#{uuid}', name: `#{name}'>"
end

private

def generate_uuid
_uuid = CFUUIDCreate(nil)
uuid = CFUUIDCreateString(nil, _uuid)
CFRelease(_uuid)
CFMakeCollectable(uuid)
# Xcode's version is actually shorter, not worrying about collisions too much right now.
uuid.gsub('-', '')[0..23]
end

def list_by_class(uuids, klass, scoped = nil)
unless scoped
scoped = uuids.map { |uuid| @project.objects[uuid] }.select { |o| o.is_a?(klass) }
end
PBXObjectList.new(klass, @project, scoped) do |object|
# Add the uuid of a newly created object to the uuids list
uuids << object.uuid
end
end
end

class PBXGroup < PBXObject
attributes_accessor :sourceTree, :children

def initialize(project, uuid, attributes)
super
self.sourceTree ||= '<group>'
self.children ||= []
end

def files
list_by_class(children, PBXFileReference)
end

def source_files
list_by_class(children, PBXFileReference, files.select { |file| !file.build_file.nil? })
end

def groups
list_by_class(children, PBXGroup)
end

def add_source_file(path, copy_header_phase = nil, compiler_flags = nil)
file = files.new('path' => path.to_s)
build_file = file.build_file
if path.extname == '.h'
build_file.settings = { 'ATTRIBUTES' => ["Public"] }
# Working around a bug in Xcode 4.2 betas, remove this once the Xcode bug is fixed:
# https://github.com/alloy/cocoapods/issues/13
#phase = copy_header_phase || @project.headers_build_phases.first
phase = copy_header_phase || @project.copy_files_build_phases.first # TODO is this really needed?
phase.files << build_file
else
build_file.settings = { 'COMPILER_FLAGS' => compiler_flags } if compiler_flags
@project.source_build_phase.files << build_file
end
file
end
end

class PBXFileReference < PBXObject
attributes_accessor :path, :sourceTree

def initialize(project, uuid, attributes)
is_new = uuid.nil?
super
self.name ||= pathname.basename.to_s
self.sourceTree ||= 'SOURCE_ROOT'
if is_new
@project.build_files.new.file = self
end
end

def pathname
Pathname.new(path)
end

def build_file
@project.build_files.find { |o| o.fileRef == uuid }
end
end

class PBXBuildFile < PBXObject
attributes_accessor :fileRef, :settings

# Takes a PBXFileReference instance and assigns its uuid to the fileRef attribute.
def file=(file)
self.fileRef = file.uuid
end

# Returns a PBXFileReference instance corresponding to the uuid in the fileRef attribute.
def file
project._objects[fileRef]
end
end

class PBXBuildPhase < PBXObject
attributes_accessor :files
alias_method :file_uuids, :files
alias_method :file_uuids=, :files=

def files
list_by_class(file_uuids, PBXBuildFile)
end
end
class PBXSourcesBuildPhase < PBXBuildPhase; end
class PBXCopyFilesBuildPhase < PBXBuildPhase; end

class PBXNativeTarget < PBXObject
attributes_accessor :buildPhases
alias_method :build_phase_uuids, :buildPhases
alias_method :build_phase_uuids=, :buildPhases=

def buildPhases
list_by_class(build_phase_uuids, PBXBuildPhase)
end
end

# Missing constants that begin with either `PBX' or `XC' are assumed to be
# valid classes in a Xcode project. A new PBXObject subclass is created
# for the constant and returned.
def self.const_missing(name)
if name =~ /^(PBX|XC)/
klass = Class.new(PBXObject)
const_set(name, klass)
klass
else
super
end
end

class PBXObjectList
include Enumerable

def initialize(represented_class, project, scoped, &new_object_callback)
@represented_class = represented_class
@project = project
@scoped_hash = scoped.is_a?(Array) ? scoped.inject({}) { |h, o| h[o.uuid] = o.attributes; h } : scoped
@callback = new_object_callback
end

def [](uuid)
if hash = @scoped_hash[uuid]
Project.const_get(hash['isa']).new(@project, uuid, hash)
end
end

def add(klass, hash = {})
object = klass.new(@project, nil, hash)
@project.objects_hash[object.uuid] = object.attributes
object
end

def new(hash = {})
object = add(@represented_class, hash)
@callback.call(object) if @callback
object
end

def <<(object)
@callback.call(object) if @callback
end

def each
@scoped_hash.keys.each do |uuid|
yield self[uuid]
end
end

def inspect
"<PBXObjectList: #{map(&:inspect)}>"
end

# Only makes sense on the list that has the full objects_hash as its scoped hash.
def select_by_class(klass)
scoped = @project.objects_hash.select { |_, attr| attr['isa'] == klass.isa }
PBXObjectList.new(klass, @project, scoped)
end
end

include Pod::Config::Mixin

# TODO this is a workaround for an issue with MacRuby with compiled files
Expand Down Expand Up @@ -31,56 +237,58 @@ def to_hash
@template
end

def find_objects(conditions)
objects.select do |_, object|
object.objectsForKeys(conditions.keys, notFoundMarker:Object.new) == conditions.values
end
def objects_hash
@template['objects']
end

def objects
@objects ||= PBXObjectList.new(PBXObject, self, objects_hash)
end

def groups
objects.select_by_class(PBXGroup)
end

# Shortcut access to the `Pods' PBXGroup.
def pods
groups.find { |g| g.name == 'Pods' }
end

# Adds a group as child to the `Pods' group.
def add_pod_group(name)
pods.groups.new('name' => name)
end

def files
objects.select_by_class(PBXFileReference)
end

def build_files
objects.select_by_class(PBXBuildFile)
end

def find_object(conditions)
find_objects(conditions).first
def source_build_phase
objects.find { |o| o.is_a?(PBXSourcesBuildPhase) }
end

def copy_files_build_phases
objects.select_by_class(PBXCopyFilesBuildPhase)
end

def targets
objects.select_by_class(PBXNativeTarget)
end

IGNORE_GROUPS = ['Pods', 'Frameworks', 'Products', 'Supporting Files']
def source_files
source_files = {}
find_objects('isa' => 'PBXGroup').each do |_, object|
next if object['name'].nil? || IGNORE_GROUPS.include?(object['name'])
source_files[object['name']] = object['children'].map do |uuid|
Pathname.new(objects[uuid]['path'])
end
groups.each do |group|
next if group.name.nil? || IGNORE_GROUPS.include?(group.name)
source_files[group.name] = group.source_files.map(&:pathname)
end
source_files
end

def add_source_file(file, group, phase_uuid = nil, compiler_flags = nil)
file_ref_uuid = add_file_reference(file, 'SOURCE_ROOT')
add_object_to_group(file_ref_uuid, group)
if file.extname == '.h'
build_file_uuid = add_build_file(file_ref_uuid, "settings" => { "ATTRIBUTES" => ["Public"] })
# Working around a bug in Xcode 4.2 betas, remove this once the Xcode bug is fixed:
# https://github.com/alloy/cocoapods/issues/13
#add_file_to_list('PBXHeadersBuildPhase', build_file_uuid)
add_file_to_list('PBXCopyFilesBuildPhase', build_file_uuid, phase_uuid)
else
extra = compiler_flags ? {"settings" => { "COMPILER_FLAGS" => compiler_flags }} : {}
build_file_uuid = add_build_file(file_ref_uuid, extra)
add_file_to_list('PBXSourcesBuildPhase', build_file_uuid)
end
file_ref_uuid
end

def add_group(name)
group_uuid = add_object({
"name" => name,
"isa" => "PBXGroup",
"sourceTree" => "<group>",
"children" => []
})
add_object_to_group(group_uuid, 'Pods')
group_uuid
end

def create_in(pods_root)
puts " * Copying contents of template directory `#{@template_dir}' to `#{pods_root}'" if config.verbose?
FileUtils.cp_r("#{@template_dir}/.", pods_root)
Expand All @@ -91,83 +299,18 @@ def create_in(pods_root)

# TODO add comments, or even constants, describing what these magic numbers are.
def add_copy_header_build_phase(name, path)
phase_uuid = add_object({
"isa" => "PBXCopyFilesBuildPhase",
phase = copy_files_build_phases.new({
"buildActionMask" => "2147483647",
"dstPath" => "$(PUBLIC_HEADERS_FOLDER_PATH)/#{path}",
"dstSubfolderSpec" => "16",
"files" => [],
"name" => "Copy #{name} Public Headers",
"runOnlyForDeploymentPostprocessing" => "0",
})

object_uuid, object = objects_by_isa('PBXNativeTarget').first
object['buildPhases'] << phase_uuid
phase_uuid
end

def objects_by_isa(isa)
objects.select { |_, object| object['isa'] == isa }
end

private

def add_object(object)
uuid = generate_uuid
objects[uuid] = object
uuid
end

def add_file_reference(path, source_tree)
add_object({
"name" => path.basename.to_s,
"isa" => "PBXFileReference",
"sourceTree" => source_tree,
"path" => path.to_s,
})
end

def add_build_file(file_ref_uuid, extra = {})
add_object(extra.merge({
"isa" => "PBXBuildFile",
"fileRef" => file_ref_uuid
}))
end

# TODO refactor to create PBX object classes and make this take aither a uuid or a class instead of both.
def add_file_to_list(isa, build_file_uuid, phase_uuid = nil)
objects = objects_by_isa(isa)
_ = object = nil
if phase_uuid.nil?
_, object = objects.first
else
object = objects[phase_uuid]
end
object['files'] << build_file_uuid
end

def add_object_to_group(object_ref_uuid, name)
object_uuid, object = objects.find do |_, object|
object['isa'] == 'PBXGroup' && object['name'] == name
end
object['children'] << object_ref_uuid
targets.first.buildPhases << phase
phase.uuid
end

def objects
@template['objects']
end

def generate_uuid
_uuid = CFUUIDCreate(nil)
uuid = CFUUIDCreateString(nil, _uuid)
CFRelease(_uuid)
CFMakeCollectable(uuid)
# Xcode's version is actually shorter, not worrying about collisions too much right now.
uuid.gsub('-', '')[0..23]
end

public

# A silly hack to pretty print the objects hash from MacRuby.
def pretty_print
puts `ruby -r pp -e 'pp(#{@template.inspect})'`
end
Expand Down
Loading

0 comments on commit ffefba9

Please sign in to comment.