Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ruby implementation of xcproj functionality #203

Merged
merged 21 commits into from
Jan 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

##### Enhancements

* Use the `DVTFoundation.framework` of Xcode to serialize projects as ASCII
plists. This makes the optional installation of `xcproj` unnecessary to
retain the project file format.
[Boris Bügling](https://github.com/neonichu)
[Xcodeproj#199](https://github.com/CocoaPods/Xcodeproj/issues/199)
[Xcodeproj#203](https://github.com/CocoaPods/Xcodeproj/issues/203)

* `PlistHelper`: Add support for plist files with numbers (`real`, `integer`).
[Vincent Isambart](https://github.com/vincentisambart)

Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ by performing the following command:

$ [sudo] gem install xcodeproj

To make Xcodeproj output projects in the same format of Xcode (deprecated ASCII Plists format) in order to reduce the SCM noise it is possible to install [xcproj](https://github.com/0xced/xcproj):

$ brew install xcproj

## Collaborate

All Xcodeproj development happens on [GitHub][xcodeproj]. Contributing patches
Expand Down
203 changes: 201 additions & 2 deletions lib/xcodeproj/plist_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ def write(hash, path)
path = path.to_s
raise IOError, 'Empty path.' if path == ''

CoreFoundation.RubyHashPropertyListWrite(hash, path)
if DevToolsCore.load_xcode_frameworks && path.end_with?('pbxproj')
ruby_hash_write_xcode(hash, path)
else
CoreFoundation.RubyHashPropertyListWrite(hash, path)
end
end

# @return [Hash] Returns the native objects loaded from a property list
Expand All @@ -59,6 +63,8 @@ def read(path)
CoreFoundation.RubyHashPropertyListRead(path)
end

private

# @return [Bool] Checks whether there are merge conflicts in the file.
#
# @param [#to_s] path
Expand All @@ -70,6 +76,32 @@ def file_in_conflict?(path)
ensure
file.close
end

# Serializes a hash as an ASCII plist, using Xcode.
#
# @param [Hash] hash
# The hash to store.
#
# @param [String] path
# The path of the file.
#
def ruby_hash_write_xcode(hash, path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neonichu Could you add some documentation to this stating which type this method expects. I.e, path being a pathname or string etc.

path = File.expand_path(path)
success = true

begin
plist = DevToolsCore::CFDictionary.new(CoreFoundation.RubyHashToCFDictionary(hash))
data = DevToolsCore::NSData.new(plist.plistDescriptionUTF8Data)
success &= data.writeToFileAtomically(path)

project = DevToolsCore::PBXProject.new(path)
success &= project.writeToFileSystemProjectFile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This expects an absolute path, otherwise the following assertion is hit:

2014-10-18 12:33:59.642 ruby[93754:910280] [MT] DVTAssertions: ASSERTION FAILURE in /SourceCache/IDEXcode3ProjectSupport/IDEXcode3ProjectSupport-6263/Xcode3Core/LegacyProjects/Frameworks/DevToolsCore/DevToolsCore/RuntimeSupport/SDKs/XCSDKPackage.m:202
Details:  Assertion failed: [basePath isAbsolutePath]
Object:   <XCSDKPackage>
Method:   +sdkPackageForNameOrPath:withBasePath:forceCreate:
Thread:   <NSThread: 0x7fc1c422e7a0>{number = 1, name = main}
Hints: None
Backtrace:
  0  0x00000001097fbf9a -[DVTAssertionHandler handleFailureInMethod:object:fileName:lineNumber:assertionSignature:messageFormat:arguments:] (in DVTFoundation)
  1  0x00000001097fb9ef _DVTAssertionHandler (in DVTFoundation)
  2  0x00000001097fbcde _DVTAssertionFailureHandler (in DVTFoundation)
  3  0x00000001097fbc40 _DVTAssertionFailureHandler (in DVTFoundation)
  4  0x000000010b29d8c7 +[XCSDKPackage sdkPackageForNameOrPath:withBasePath:forceCreate:] (in DevToolsCore)
  5  0x000000010b15b32d -[PBXTarget createMacroExpansionScopeWithBuildParameters:] (in DevToolsCore)
  6  0x000000010b15cc07 -[PBXTarget cachedMacroExpansionScopeForBuildParameters:] (in DevToolsCore)
  7  0x000000010b17199f -[PBXTarget expandedValueForString:forBuildParameters:] (in DevToolsCore)
  8  0x000000010b1e8c0b -[PBXNativeTarget fullProductNameForConfigurationNamed:] (in DevToolsCore)
  9  0x000000010b16adb9 -[PBXTarget fullProductName] (in DevToolsCore)
 10  0x000000010b12ae97 -[PBXReference setProducingTarget:] (in DevToolsCore)
 11  0x000000010b16f4e3 -[PBXTarget awakeFromPListUnarchiver:] (in DevToolsCore)
 12  0x000000010b1e99b8 -[PBXNativeTarget awakeFromPListUnarchiver:] (in DevToolsCore)
 13  0x00007fff92fc00d5 -[NSArray makeObjectsPerformSelector:withObject:] (in CoreFoundation)
 14  0x000000010b1419b5 -[PBXPListUnarchiver decodeRootObject] (in DevToolsCore)
 15  0x000000010b10c419 +[PBXProject projectWithFile:errorHandler:readOnly:] (in DevToolsCore)
 16  0x000000010b133e77 -[PBXFileReference loadedContainer] (in DevToolsCore)
 17  0x000000010b207f3f -[PBXContainerItemProxy remoteContainer] (in DevToolsCore)
 18  0x000000010b20800f -[PBXContainerItemProxy remoteContainerItem] (in DevToolsCore)
 19  0x000000010b20ad51 -[PBXReferenceProxy sourceTree] (in DevToolsCore)
 20  0x000000010b12a537 -[PBXReference addNotifications] (in DevToolsCore)
 21  0x000000010b130e11 -[PBXReference awakeFromPListUnarchiver:] (in DevToolsCore)
 22  0x000000010b20bd3b -[PBXReferenceProxy awakeFromPListUnarchiver:] (in DevToolsCore)
 23  0x00007fff92fbffd6 -[NSArray makeObjectsPerformSelector:withObject:] (in CoreFoundation)
 24  0x000000010b1419b5 -[PBXPListUnarchiver decodeRootObject] (in DevToolsCore)
 25  0x000000010b10c419 +[PBXProject projectWithFile:errorHandler:readOnly:] (in DevToolsCore)
 26  0x000000010b10d30d +[PBXProject projectWithFile:errorHandler:] (in DevToolsCore)
 27  0x00007fff8d9a2f44 ffi_call_unix64 (in libffi.dylib)
 28  0x00007fff569dff10
fish: Job 1, 'irb -I lib/xcodeproj -r plist_helper' terminated by signal SIGABRT (Abort)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ef7dbf6

rescue Fiddle::DLError
success = false
end

CoreFoundation.RubyHashPropertyListWrite(hash, path) unless success
end
end
end
end
Expand Down Expand Up @@ -220,7 +252,7 @@ def self.CFRelease_function
@CFRelease ||= Fiddle::Function.new(image['CFRelease'], [CFTypeRef], Void)
end

def self.extern(symbol, parameter_types, return_type)
def self.extern_image(image, symbol, parameter_types, return_type)
symbol = symbol.to_s
create_function = symbol.include?('Create')
function_cache_key = "@__#{symbol}__"
Expand All @@ -244,6 +276,10 @@ def self.extern(symbol, parameter_types, return_type)
end
end

def self.extern(symbol, parameter_types, return_type)
extern_image(image, symbol, parameter_types, return_type)
end

public

# @!group CoreFoundation function definitions
Expand Down Expand Up @@ -503,3 +539,166 @@ def self.RubyBooleanToCFBoolean(value)
# rubocop:enable Style/MethodName
# rubocop:enable Style/VariableName
end

module DevToolsCore
def self.silence_stderr
begin
orig_stderr = $stderr.clone
$stderr.reopen File.new('/dev/null', 'w')
retval = yield
ensure
$stderr.reopen orig_stderr
end
retval
end

# rubocop:disable Style/MethodName
# rubocop:disable Style/VariableName

class NSObject
private

def self.objc_class
@objc_class ||= CoreFoundation.objc_getClass(name.split('::').last)
end

def self.image
@image ||= Fiddle::Handle.new
end

def self.extern(symbol, parameter_types, return_type)
CoreFoundation.extern_image(image, symbol, parameter_types, return_type)
end

def self.objc_msgSend(args, return_type = CoreFoundation::VoidPointer)
arguments = [CoreFoundation::VoidPointer, CoreFoundation::VoidPointer] + args

Fiddle::Function.new(image['objc_msgSend'], arguments, return_type)
end

def self.respondsToSelector(instance, sel)
selector = CoreFoundation.NSSelectorFromString(CoreFoundation.RubyStringToCFString(sel))
respondsToSelector = objc_msgSend([CoreFoundation::CharPointer], CoreFoundation::Boolean)
result = respondsToSelector.call(
instance,
CoreFoundation.NSSelectorFromString(CoreFoundation.RubyStringToCFString('respondsToSelector:')),
selector)
result == CoreFoundation::TRUE ? true : false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CoreFoundation.CFBooleanToRubyBoolean(result)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually not a CFBoolean, just a BOOL

end

Class = CoreFoundation::VoidPointer
ID = CoreFoundation::VoidPointer
SEL = CoreFoundation::VoidPointer

extern :NSSelectorFromString, [CoreFoundation::CFTypeRef], SEL

extern :objc_getClass, [CoreFoundation::CharPointer], Class
extern :class_getName, [Class], CoreFoundation::CharPointer
end

XCODE_PATH = Pathname.new(`xcrun xcode-select -p`.strip).dirname
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xcrun xcode-select -p won't return the path in some cases, for example:

  • Xcode not being installed

  • User not yet agreed to license

    $ xcrun xcode-select -p
    
    
    Agreeing to the Xcode/iOS license requires admin privileges, please re-run as root via sudo.
    

I'm wondering if we need to change/account for this in anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a spec for this. IMHO, we should silently fallback to XML writing, just as in other cases of not being able to use the private Xcode API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 9669942


def self.load_xcode_framework(framework)
Fiddle.dlopen(XCODE_PATH.join(framework).to_s)
rescue Fiddle::DLError
nil
end

def self.load_xcode_frameworks
load_xcode_framework('SharedFrameworks/DVTFoundation.framework/DVTFoundation')
load_xcode_framework('SharedFrameworks/DVTSourceControl.framework/DVTSourceControl')
load_xcode_framework('SharedFrameworks/CSServiceClient.framework/CSServiceClient')
load_xcode_framework('Frameworks/IDEFoundation.framework/IDEFoundation')
load_xcode_framework('PlugIns/Xcode3Core.ideplugin/Contents/MacOS/Xcode3Core')
end

class CFDictionary < NSObject
public

def initialize(dictionary)
@dictionary = dictionary
end

def plistDescriptionUTF8Data
selector = 'plistDescriptionUTF8Data'
return nil unless NSObject.respondsToSelector(@dictionary, selector)

plistDescriptionUTF8Data = CFDictionary.objc_msgSend([])
plistDescriptionUTF8Data.call(
@dictionary,
CoreFoundation.NSSelectorFromString(CoreFoundation.RubyStringToCFString(selector)))
end

def self.image
@image ||= DevToolsCore.load_xcode_frameworks
end
end

class NSData < NSObject
public

def initialize(data)
@data = data
end

def writeToFileAtomically(path)
selector = 'writeToFile:atomically:'
return false unless NSObject.respondsToSelector(@data, selector)

writeToFileAtomically = NSData.objc_msgSend([CoreFoundation::VoidPointer, CoreFoundation::Boolean], CoreFoundation::Boolean)
result = writeToFileAtomically.call(
@data,
CoreFoundation.NSSelectorFromString(CoreFoundation.RubyStringToCFString(selector)),
CoreFoundation.RubyStringToCFString(path),
1)
result == CoreFoundation::TRUE ? true : false
end
end

class PBXProject < NSObject
public

def initialize(path)
DevToolsCore.silence_stderr do
CoreFoundation.IDEInitialize(1, CoreFoundation::NULL)
CoreFoundation.XCInitializeCoreIfNeeded(1)
end

selector = 'projectWithFile:'

if NSObject.respondsToSelector(PBXProject.objc_class, selector)
projectWithFile = PBXProject.objc_msgSend([CoreFoundation::VoidPointer])
@project = projectWithFile.call(
PBXProject.objc_class,
CoreFoundation.NSSelectorFromString(CoreFoundation.RubyStringToCFString(selector)),
CoreFoundation.RubyStringToCFString(path))
end
end

def writeToFileSystemProjectFile
selector = 'writeToFileSystemProjectFile:userFile:checkNeedsRevert:'
return unless NSObject.respondsToSelector(@project, selector)

writeToFile = PBXProject.objc_msgSend([CoreFoundation::Boolean, CoreFoundation::Boolean, CoreFoundation::Boolean], CoreFoundation::Boolean)
result = writeToFile.call(
@project,
CoreFoundation.NSSelectorFromString(CoreFoundation.RubyStringToCFString(selector)),
1,
0,
1)
result == CoreFoundation::TRUE ? true : false
end

private

def self.image
@image ||= DevToolsCore.load_xcode_frameworks
end

extern :IDEInitialize, [CoreFoundation::Boolean, ID], CoreFoundation::Void
extern :XCInitializeCoreIfNeeded, [CoreFoundation::Boolean], CoreFoundation::Void
end

# rubocop:enable Style/MethodName
# rubocop:enable Style/VariableName
end
11 changes: 0 additions & 11 deletions lib/xcodeproj/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

require 'xcodeproj/project/object'
require 'xcodeproj/project/project_helper'
require 'xcodeproj/project/xcproj_helper'
require 'xcodeproj/plist_helper'

module Xcodeproj
Expand Down Expand Up @@ -156,15 +155,6 @@ def to_s

alias_method :inspect, :to_s

# @return [Boolean] Whether the `xcproj` conversion should be disabled. The
# conversion can also be disabled via the `XCODEPROJ_DISABLE_XCPROJ`
# environment variable.
#
attr_accessor :disable_xcproj
def disable_xcproj?
@disable_xcproj || ENV['XCODEPROJ_DISABLE_XCPROJ']
end

public

# @!group Initialization
Expand Down Expand Up @@ -329,7 +319,6 @@ def save(save_path = nil)
file = File.join(save_path, 'project.pbxproj')
Xcodeproj.write_plist(to_hash, file)
fix_encoding(file)
XCProjHelper.touch(save_path) unless disable_xcproj?
end

# Simple workaround to escape characters which are outside of ASCII
Expand Down
54 changes: 0 additions & 54 deletions lib/xcodeproj/project/xcproj_helper.rb

This file was deleted.

Loading