crafter 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/bin/crafter ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+
4
+ $:.push File.expand_path("../../lib", __FILE__)
5
+
6
+ if ARGV[0] == 'reset'
7
+ puts 'copying defaults to ~/.crafter.rb'
8
+ root = File.expand_path('.', File.dirname(__FILE__))
9
+ FileUtils.cp("#{root}/../lib/config/default.rb", File.join(Dir.home, '.crafter.rb'))
10
+ end
11
+
12
+ require 'crafter'
13
+ Crafter::setup_project()
data/craft.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'crafter'
6
+ gem.version = '0.1.2'
7
+ gem.authors = ['Krzysztof Zabłocki']
8
+ gem.email = ['merowing2@gmail.com']
9
+ gem.description = %q{CLI for setting up new Xcode projects. Inspired by thoughtbot liftoff.}
10
+ gem.summary = %q{Define your craft rules once, then apply it to all your Xcode projects.}
11
+ gem.homepage = 'https://github.com/krzysztofzablocki/crafter'
12
+
13
+ gem.add_dependency 'xcodeproj', '~> 0.5.5'
14
+ gem.add_dependency 'highline', '~> 1.6'
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ['lib']
20
+ end
@@ -0,0 +1,108 @@
1
+ icon_script = %q[
2
+ commit=`git rev-parse --short HEAD`
3
+ branch=`git rev-parse --abbrev-ref HEAD`
4
+ version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`
5
+
6
+ function processIcon() {
7
+ export PATH=$PATH:/usr/local/bin
8
+ base_file=$1
9
+ if [ ! -f $base_file ]; then return; fi
10
+
11
+ target_file=`echo $base_file | sed "s/_base//"`
12
+ target_path="${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${target_file}"
13
+
14
+ if [ $CONFIGURATION = "Release" ]; then
15
+ cp ${base_file} $target_path
16
+ return
17
+ fi
18
+
19
+ width=`identify -format %w ${base_file}`
20
+
21
+ convert -background '#0008' -fill white -gravity center -size ${width}x40\
22
+ caption:"${version} ${branch} ${commit}"\
23
+ ${base_file} +swap -gravity south -composite ${target_path}
24
+ }
25
+
26
+ processIcon "Icon_base.png"
27
+ processIcon "Icon_base@2x.png"
28
+ processIcon "Icon_base~iPad.png"
29
+ processIcon "Icon_base@2x~iPad.png"
30
+ ]
31
+
32
+
33
+ test_script = %q[
34
+ # Launch application using ios-sim and set up environment to inject test bundle into application
35
+ # Source: http://stackoverflow.com/a/12682617/504494
36
+
37
+ echo "ENTERING TEST SCRIPT"
38
+
39
+ if [ "$RUN_UNIT_TEST_WITH_IOS_SIM" = "YES" ]; then
40
+ echo "TESTING_"
41
+ test_bundle_path="$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.$WRAPPER_EXTENSION"
42
+ ios-sim launch "$(dirname "$TEST_HOST")" --setenv DYLD_INSERT_LIBRARIES=/../../Library/PrivateFrameworks/IDEBundleInjection.framework/IDEBundleInjection --setenv XCInjectBundle="$test_bundle_path" --setenv XCInjectBundleInto="$TEST_HOST" --args -SenTest All "$test_bundle_path"
43
+ echo "Finished running tests with ios-sim"
44
+ else
45
+ #"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests"
46
+ fi
47
+ ]
48
+
49
+ Crafter.configure do
50
+ add_platform({:platform => :ios, :deployment => 6.0})
51
+ add_git_ignore
52
+ duplicate_configurations({:adhoc => :debug, :profiling => :debug})
53
+
54
+ set_options %w(
55
+ GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED
56
+ GCC_WARN_MISSING_PARENTHESES
57
+ GCC_WARN_ABOUT_RETURN_TYPE
58
+ GCC_WARN_SIGN_COMPARE
59
+ GCC_WARN_CHECK_SWITCH_STATEMENTS
60
+ GCC_WARN_UNUSED_FUNCTION
61
+ GCC_WARN_UNUSED_LABEL
62
+ GCC_WARN_UNUSED_VALUE
63
+ GCC_WARN_UNUSED_VARIABLE
64
+ GCC_WARN_SHADOW
65
+ GCC_WARN_64_TO_32_BIT_CONVERSION
66
+ GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS
67
+ GCC_WARN_UNDECLARED_SELECTOR
68
+ GCC_WARN_TYPECHECK_CALLS_TO_PRINTF
69
+
70
+ RUN_CLANG_STATIC_ANALYZER
71
+ GCC_TREAT_WARNINGS_AS_ERRORS
72
+ )
73
+
74
+ with :default do
75
+ pods << %w(NSLogger-CocoaLumberjack-connector TestFlightSDK)
76
+
77
+ add_option :networking do
78
+ pods << 'AFNetworking'
79
+ end
80
+
81
+
82
+ add_option :coredata do
83
+ pods << 'MagicalRecord'
84
+ end
85
+
86
+ # icon versioning
87
+ scripts << {:name => 'icon versioning', :script => icon_script}
88
+ icon_rename = proc do |file|
89
+ extension = File.extname(file)
90
+ file_name = File.basename(file, extension)
91
+ File.rename(file, "#{File.dirname(file)}/#{file_name}_base.#{extension}")
92
+ end
93
+
94
+ Dir['**/Icon.png'].each(&icon_rename)
95
+ Dir['**/Icon@2x.png'].each(&icon_rename)
96
+ Dir['**/Icon-72.png'].each(&icon_rename)
97
+ Dir['**/Icon-72@2x.png'].each(&icon_rename)
98
+ end
99
+
100
+
101
+ with :tests do
102
+ add_option :testing do
103
+ pods << 'Kiwi'
104
+ scripts << {:name => 'command line unit tests', :script => test_script}
105
+ end
106
+ end
107
+
108
+ end
data/lib/crafter.rb ADDED
@@ -0,0 +1,103 @@
1
+ require_relative 'project_helper'
2
+ require_relative 'git_helper'
3
+ require_relative 'target_configuration'
4
+
5
+ module Crafter
6
+ extend self
7
+ Crafter::ROOT = File.expand_path('.', File.dirname(__FILE__))
8
+
9
+ @targets = {}
10
+ @add_git_ignore = false
11
+ @platforms = []
12
+
13
+ def configure(&block)
14
+ instance_eval &block
15
+ end
16
+
17
+ def with(name, &block)
18
+ self.find_target_configuration(name).instance_eval(&block)
19
+ end
20
+
21
+ def project
22
+ @project ||= ProjectHelper.new
23
+ end
24
+
25
+ def find_target_configuration(name)
26
+ target = @targets[name]
27
+ target ||= self.project.select_target_for_name name
28
+ target_configuration = TargetConfiguration.new(name, target)
29
+ @targets[name] = target_configuration
30
+ target_configuration
31
+ end
32
+
33
+ def add_platform(platform_hash)
34
+ @platforms << platform_hash
35
+ end
36
+
37
+ def add_git_ignore
38
+ @add_git_ignore = true
39
+ end
40
+
41
+ def duplicate_configurations(duplication_hash)
42
+ @configuration = duplication_hash
43
+ end
44
+
45
+ def set_options(options)
46
+ @options = options
47
+ end
48
+
49
+ def setup_project
50
+ process_optional()
51
+ process_configurations() unless @configuration.empty?
52
+ process_options() unless @options.empty?
53
+ process_git() if @add_git_ignore
54
+ process_pods()
55
+ process_scripts()
56
+ end
57
+
58
+ def process_configurations
59
+ self.project.duplicate_configurations(@configuration)
60
+ end
61
+
62
+ def process_optional
63
+ @targets.each { |_, v| v.process_optional }
64
+ end
65
+
66
+ def process_options
67
+ self.project.enable_options(@options)
68
+ end
69
+
70
+ def process_git
71
+ GitHelper.new.generate_files
72
+ end
73
+
74
+ def process_pods
75
+ File.open('Podfile', File::WRONLY|File::CREAT|File::EXCL) do |f|
76
+
77
+ @platforms.each do |hash|
78
+ name = hash[:platform]
79
+ deployment = hash[:deployment]
80
+ if deployment
81
+ f.puts "platform :#{name}, '#{deployment}'"
82
+ else
83
+ f.puts "platform #{name}"
84
+ end
85
+ end
86
+
87
+ @targets.each { |_, v| v.write_pods(f) }
88
+ end
89
+
90
+ rescue Exception => e
91
+ puts "Skipping pod generation - #{e}"
92
+ end
93
+
94
+ def process_scripts
95
+ @targets.each { |_, v| v.process_scripts(self.project) }
96
+ end
97
+
98
+ if File.exists?(File.join(Dir.home, '.crafter.rb')) then
99
+ load '~/.crafter.rb'
100
+ else
101
+ load "#{Crafter::ROOT}/config/default.rb"
102
+ end
103
+ end
data/lib/git_helper.rb ADDED
@@ -0,0 +1,93 @@
1
+ #Copyright (c) 2012-2013 thoughtbot, inc.
2
+ #
3
+ # MIT License
4
+ #
5
+ #Permission is hereby granted, free of charge, to any person obtaining
6
+ #a copy of this software and associated documentation files (the
7
+ #"Software"), to deal in the Software without restriction, including
8
+ #without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ #permit persons to whom the Software is furnished to do so, subject to
11
+ #the following conditions:
12
+ #
13
+ #The above copyright notice and this permission notice shall be
14
+ #included in all copies or substantial portions of the Software.
15
+ #
16
+ #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ #EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ #MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ #NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ #LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ #OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ #WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ GITIGNORE_CONTENTS = <<GITIGNORE
25
+ # OS X Finder
26
+ .DS_Store
27
+
28
+ # Xcode per-user config
29
+ *.mode1
30
+ *.mode1v3
31
+ *.mode2v3
32
+ *.perspective
33
+ *.perspectivev3
34
+ *.pbxuser
35
+ xcuserdata
36
+
37
+ # Build products
38
+ build/
39
+ *.o
40
+ *.LinkFileList
41
+ *.hmap
42
+
43
+ # Automatic backup files
44
+ *~.nib/
45
+ *.swp
46
+ *~
47
+ *.dat
48
+ *.dep
49
+
50
+ # AppCode
51
+ .idea
52
+
53
+ GITIGNORE
54
+
55
+ GITATTRIBUTES_CONTENTS = '*.pbxproj binary merge=union'
56
+
57
+ class GitHelper
58
+ def initialize
59
+ if Dir['*.xcodeproj'].empty?
60
+ puts 'Could not find an Xcode project file. You need to run me from a valid project directory.'
61
+ exit
62
+ end
63
+ end
64
+
65
+ def generate_files
66
+ generate_gitignore
67
+ generate_gitattributes
68
+ end
69
+
70
+ private
71
+
72
+ def generate_gitignore
73
+ write_unique_contents_to_file(GITIGNORE_CONTENTS, '.gitignore')
74
+ end
75
+
76
+ def generate_gitattributes
77
+ write_unique_contents_to_file(GITATTRIBUTES_CONTENTS, '.gitattributes')
78
+ end
79
+
80
+ def write_unique_contents_to_file(contents, filename)
81
+ if File.exists? filename
82
+ current_file_contents = File.read(filename).split("\n")
83
+ else
84
+ current_file_contents = []
85
+ end
86
+
87
+ new_contents = current_file_contents + contents.split("\n")
88
+
89
+ File.open(filename, 'w') do |file|
90
+ file.write(new_contents.uniq.join("\n"))
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,102 @@
1
+ require 'xcodeproj'
2
+ require 'highline/import'
3
+
4
+ class ProjectHelper
5
+ PROJECTS = Dir.glob('*.xcodeproj')
6
+
7
+ def initialize
8
+ @project = Xcodeproj::Project.new(xcode_project_file)
9
+ end
10
+
11
+ def enable_options(options)
12
+ @project.build_configurations.each do |configuration|
13
+ options.each do |option|
14
+ configuration.build_settings[option] = 'YES'
15
+ end
16
+ end
17
+ save_changes
18
+ end
19
+
20
+ def add_shell_script(target, name, script)
21
+ if target.shell_script_build_phases.to_a.index { |phase| phase.name == name }
22
+ puts "Skipping adding shell script for target #{target} as #{name} already exists"
23
+ else
24
+ target.new_shell_script_build_phase(name).shell_script = script
25
+ save_changes
26
+ end
27
+ end
28
+
29
+ def duplicate_configurations(configurations_hash)
30
+ configurations_hash.each do |name, base|
31
+
32
+ @project.targets.each do |target|
33
+ base_configuration, project_configuration = find_configurations(base, target)
34
+
35
+ if !base_configuration || !project_configuration
36
+ puts "unable to find configurations for #{base}"
37
+ next
38
+ end
39
+
40
+ target.build_configurations << clone_configuration(base_configuration, name)
41
+ @project.build_configurations << clone_configuration(project_configuration, name)
42
+ end
43
+ end
44
+
45
+ save_changes
46
+ end
47
+
48
+ def select_target_for_name(name)
49
+ targets = @project.targets.to_a.select { |t| t.name.end_with? name.to_s }
50
+ targets = @project.targets.to_a if targets.empty?
51
+ choose_item("Which target should I use for #{name}?", targets)
52
+ end
53
+
54
+ private
55
+
56
+ def clone_configuration(base_configuration, name)
57
+ build_config = @project.new(Xcodeproj::Project::XCBuildConfiguration)
58
+ build_config.name = name.to_s
59
+ build_config.build_settings = base_configuration
60
+ build_config
61
+ end
62
+
63
+ def find_configurations(base, target)
64
+ base_configuration = target.build_configuration_list.build_configurations.find { |t| t.name.downcase == base.to_s.downcase }
65
+ base_configuration = base_configuration.build_settings if base_configuration
66
+
67
+ project_configuration = @project.build_configurations.find { |t| t.name.downcase == base.to_s.downcase }
68
+ project_configuration = project_configuration.build_settings if project_configuration
69
+ return base_configuration, project_configuration
70
+ end
71
+
72
+ def xcode_project_file
73
+ @xcode_project_file ||= choose_item('Project', PROJECTS)
74
+
75
+ if @xcode_project_file == 'Pods.xcodeproj'
76
+ raise 'Can not run in the Pods directory. $ cd .. maybe?'
77
+ end
78
+
79
+ @xcode_project_file
80
+ end
81
+
82
+ def choose_item(title, objects)
83
+ if objects.empty?
84
+ raise "Could not locate any #{title}s!"
85
+ elsif objects.size == 1
86
+ objects.first
87
+ else
88
+ choose do |menu|
89
+ menu.prompt = title
90
+ objects.map { |object| menu.choice(object) }
91
+ end
92
+ end
93
+ end
94
+
95
+ def available_targets
96
+ @project.targets.to_a.delete_if { |t| t.name.end_with?('Tests') }
97
+ end
98
+
99
+ def save_changes
100
+ @project.save_as xcode_project_file
101
+ end
102
+ end
@@ -0,0 +1,59 @@
1
+ class TargetConfiguration
2
+ attr_accessor :pods
3
+ attr_accessor :scripts
4
+
5
+ def initialize(name, target)
6
+ @name = name
7
+ @target = target
8
+ @pods = []
9
+ @scripts = []
10
+ @options = {}
11
+ end
12
+
13
+ def add_option(name, &block)
14
+ @options[name] = block
15
+ end
16
+
17
+ def process_optional
18
+ @options.each do |key, obj|
19
+ key_string = key.to_s
20
+ if ask_question "do you want to add #{key_string}?"
21
+ raise unless obj.is_a? Proc
22
+ obj.call()
23
+ end
24
+ end
25
+ end
26
+
27
+ def write_pods(f)
28
+ f.puts "target :#{@target.name}, :exclusive => true do"
29
+ pods.flatten.each do |pod|
30
+ f.puts "pod '#{pod}'"
31
+ end
32
+ f.puts 'end'
33
+ end
34
+
35
+ def process_scripts(project)
36
+ scripts.each do |hash|
37
+ project.add_shell_script(@target, hash[:name], hash[:script])
38
+ end
39
+ end
40
+
41
+ def ask_question(question)
42
+ puts question
43
+ get_input()
44
+ end
45
+
46
+ def get_input
47
+ STDOUT.flush
48
+ input = STDIN.gets.chomp
49
+ case input.upcase
50
+ when 'Y'
51
+ true
52
+ when 'N'
53
+ false
54
+ else
55
+ puts 'Please enter Y or N'
56
+ get_input
57
+ end
58
+ end
59
+ end