Commit ffefba98 authored by Eloy Duran's avatar Eloy Duran

Refactor Pod::Xcode::Project to use actual objects instead of raw hashes.

parent 7c8ac07b
......@@ -3,6 +3,212 @@ framework 'Foundation'
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
......@@ -31,54 +237,56 @@ module Pod
@template
end
def find_objects(conditions)
objects.select do |_, object|
object.objectsForKeys(conditions.keys, notFoundMarker:Object.new) == conditions.values
def objects_hash
@template['objects']
end
def objects
@objects ||= PBXObjectList.new(PBXObject, self, objects_hash)
end
def find_object(conditions)
find_objects(conditions).first
def groups
objects.select_by_class(PBXGroup)
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'])
# 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
source_files
def files
objects.select_by_class(PBXFileReference)
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)
def build_files
objects.select_by_class(PBXBuildFile)
end
file_ref_uuid
def source_build_phase
objects.find { |o| o.is_a?(PBXSourcesBuildPhase) }
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
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 = {}
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 create_in(pods_root)
......@@ -91,83 +299,18 @@ module Pod
# 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]
targets.first.buildPhases << phase
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
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
......
......@@ -7,15 +7,90 @@ describe "Pod::Xcode::Project" do
@project = Pod::Xcode::Project.ios_static_library
end
def find_objects(conditions)
@project.objects_hash.select do |_, object|
object.objectsForKeys(conditions.keys, notFoundMarker:Object.new) == conditions.values
end
end
def find_object(conditions)
find_objects(conditions).first
end
it "returns an instance initialized from the iOS static library template" do
template_dir = Pod::Xcode::Project::TEMPLATES_DIR + 'cocoa-touch-static-library'
template_file = (template_dir + 'Pods.xcodeproj/project.pbxproj').to_s
@project.to_hash.should == NSDictionary.dictionaryWithContentsOfFile(template_file)
end
it "returns the objects hash" do
@project.objects_hash.should == @project.to_hash['objects']
end
describe "PBXObject" do
before do
@object = Pod::Xcode::Project::PBXObject.new(@project.objects, nil, 'name' => 'AnObject')
end
it "merges the class name into the attributes" do
@object.isa.should == 'PBXObject'
@object.attributes['isa'].should == 'PBXObject'
end
it "takes a name" do
@object.name.should == 'AnObject'
@object.name = 'AnotherObject'
@object.name.should == 'AnotherObject'
end
it "generates a uuid" do
@object.uuid.should.be.instance_of String
@object.uuid.size.should == 24
@object.uuid.should == @object.uuid
end
end
it "returns the objects as PBXObject instances" do
@project.objects.each do |object|
@project.objects_hash[object.uuid].should == object.attributes
end
end
it "adds any type of new PBXObject to the objects hash" do
object = @project.objects.add(Pod::Xcode::Project::PBXObject, 'name' => 'An Object')
object.name.should == 'An Object'
@project.objects_hash[object.uuid].should == object.attributes
end
it "adds a new PBXObject, of the configured type, to the objects hash" do
group = @project.groups.new('name' => 'A new group')
group.isa.should == 'PBXGroup'
group.name.should == 'A new group'
@project.objects_hash[group.uuid].should == group.attributes
end
it "adds a new PBXFileReference to the objects hash" do
file = @project.files.new('path' => '/some/file.m')
file.isa.should == 'PBXFileReference'
file.name.should == 'file.m'
file.path.should == '/some/file.m'
file.sourceTree.should == 'SOURCE_ROOT'
@project.objects_hash[file.uuid].should == file.attributes
end
it "adds a new PBXBuildFile to the objects hash when a new PBXFileReference is created" do
file = @project.files.new('name' => '/some/source/file.h')
build_file = file.build_file
build_file.file = file
build_file.fileRef.should == file.uuid
build_file.isa.should == 'PBXBuildFile'
@project.objects_hash[build_file.uuid].should == build_file.attributes
end
it "adds a group to the `Pods' group" do
@project.add_group('JSONKit')
@project.find_object({
group = @project.add_pod_group('JSONKit')
@project.pods.children.should.include group.uuid
find_object({
'isa' => 'PBXGroup',
'name' => 'JSONKit',
'sourceTree' => '<group>',
......@@ -25,32 +100,32 @@ describe "Pod::Xcode::Project" do
it "adds an `m' or `c' file as a build file, adds it to the specified group, and adds it to the `sources build' phase list" do
file_ref_uuids, build_file_uuids = [], []
group_uuid = @project.add_group('SomeGroup')
group = @project.to_hash['objects'][group_uuid]
group = @project.add_pod_group('SomeGroup')
%w{ m mm c cpp }.each do |ext|
path = Pathname.new("path/to/file.#{ext}")
file_ref_uuid = @project.add_source_file(path, 'SomeGroup')
@project.to_hash['objects'][file_ref_uuid].should == {
file = group.add_source_file(path)
@project.objects_hash[file.uuid].should == {
'name' => path.basename.to_s,
'isa' => 'PBXFileReference',
'sourceTree' => 'SOURCE_ROOT',
'path' => path.to_s
}
file_ref_uuids << file_ref_uuid
file_ref_uuids << file.uuid
build_file_uuid, _ = @project.find_object({
build_file_uuid, _ = find_object({
'isa' => 'PBXBuildFile',
'fileRef' => file_ref_uuid
'fileRef' => file.uuid
})
build_file_uuids << build_file_uuid
group['children'].should == file_ref_uuids
group.children.should == file_ref_uuids
_, object = @project.find_object('isa' => 'PBXSourcesBuildPhase')
_, object = find_object('isa' => 'PBXSourcesBuildPhase')
object['files'].should == build_file_uuids
_, object = @project.find_object('isa' => 'PBXHeadersBuildPhase')
_, object = find_object('isa' => 'PBXHeadersBuildPhase')
object['files'].should.not.include build_file_uuid
end
end
......@@ -59,10 +134,10 @@ describe "Pod::Xcode::Project" do
build_file_uuids = []
%w{ m mm c cpp }.each do |ext|
path = Pathname.new("path/to/file.#{ext}")
file_ref_uuid = @project.add_source_file(path, 'Pods', nil, '-fno-obj-arc')
@project.find_object({
file = @project.pods.add_source_file(path, nil, '-fno-obj-arc')
find_object({
'isa' => 'PBXBuildFile',
'fileRef' => file_ref_uuid,
'fileRef' => file.uuid,
'settings' => {'COMPILER_FLAGS' => '-fno-obj-arc' }
}).should.not == nil
end
......@@ -70,36 +145,35 @@ describe "Pod::Xcode::Project" do
it "creates a copy build header phase which will copy headers to a specified path" do
phase_uuid = @project.add_copy_header_build_phase("SomePod", "Path/To/Source")
@project.find_object({
find_object({
'isa' => 'PBXCopyFilesBuildPhase',
'dstPath' => '$(PUBLIC_HEADERS_FOLDER_PATH)/Path/To/Source',
'name' => 'Copy SomePod Public Headers'
}).should.not == nil
_, target = @project.objects_by_isa('PBXNativeTarget').first
target['buildPhases'].should.include phase_uuid
target = @project.targets.first
target.attributes['buildPhases'].should.include phase_uuid
end
it "adds a `h' file as a build file and adds it to the `headers build' phase list" do
@project.add_group('SomeGroup')
group = @project.groups.new('name' => 'SomeGroup')
path = Pathname.new("path/to/file.h")
file_ref_uuid = @project.add_source_file(path, 'SomeGroup')
@project.to_hash['objects'][file_ref_uuid].should == {
file = group.add_source_file(path)
@project.objects_hash[file.uuid].should == {
'name' => path.basename.to_s,
'isa' => 'PBXFileReference',
'sourceTree' => 'SOURCE_ROOT',
'path' => path.to_s
}
build_file_uuid, _ = @project.find_object({
build_file_uuid, _ = find_object({
'isa' => 'PBXBuildFile',
'fileRef' => file_ref_uuid
'fileRef' => file.uuid
})
#_, object = @project.find_object('isa' => 'PBXHeadersBuildPhase')
_, object = @project.find_object('isa' => 'PBXCopyFilesBuildPhase')
#_, object = find_object('isa' => 'PBXHeadersBuildPhase')
_, object = find_object('isa' => 'PBXCopyFilesBuildPhase')
object['files'].should == [build_file_uuid]
_, object = @project.find_object('isa' => 'PBXSourcesBuildPhase')
_, object = find_object('isa' => 'PBXSourcesBuildPhase')
object['files'].should.not.include build_file_uuid
end
......@@ -112,9 +186,9 @@ describe "Pod::Xcode::Project" do
end
it "returns all source files" do
@project.add_group('SomeGroup')
group = @project.groups.new('name' => 'SomeGroup')
files = [Pathname.new('/some/file.h'), Pathname.new('/some/file.m')]
files.each { |file| @project.add_source_file(file, 'SomeGroup') }
@project.source_files['SomeGroup'].sort.should == files.sort
files.each { |file| group.add_source_file(file) }
group.source_files.map(&:pathname).sort.should == files.sort
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment