fruity_builder 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 388eb38a0a12e97f3742704555ca19eaca117ab8
4
+ data.tar.gz: 39cd6b97094a2b2d2ee8591f7981c25c047b4874
5
+ SHA512:
6
+ metadata.gz: e7ad3b426b016bd595b0020523a4e93412f80d24376e3627b533b6ba79856594df5a17bdc928bfbe829b7a1c59a74ecaa6a25bcd467867c65aa6b0f1bdf7d92e
7
+ data.tar.gz: 33eaacaca91a28b4819cf497280e0083df777a86e0fd991c46f6a658e02616fa2870a67e0094a137cdee82cd87f6dd8897f59e21085c18227cd9b92fc68de2f9
@@ -0,0 +1,37 @@
1
+ # Fruity Builder
2
+
3
+ Code manipulation tools for iOS code bases.
4
+
5
+ ## Usage
6
+
7
+ Initialise with a path to the project folder:
8
+
9
+ ```ruby
10
+ builder = FruityBuilder::IOS::Helper.new(path)
11
+ ```
12
+
13
+ ### Replacing build properties
14
+
15
+ ```ruby
16
+ builder.build.replace_dev_team('New dev team')
17
+ builder.build.replace_code_sign_identity('New identity')
18
+ builder.build.replace_provisioning_profile('New profile')
19
+ builder.build.save_project_properties
20
+ builder.build.replace_bundle_id('New bundle ID')
21
+ ```
22
+
23
+ ### Retrieving build properties
24
+
25
+ ```ruby
26
+ builder.build.get_dev_teams # ['Dev Team']
27
+ builder.build.get_code_signing_identities # ['Identity 1', 'Identity 2']
28
+ builder.build.get_provisioning_profiles # ['Profile 1', 'Profile 2']
29
+ ```
30
+
31
+ ### Retrieving build configurations
32
+
33
+ ```ruby
34
+ builder.xcode.get_schemes # ['Scheme 1', 'Scheme 2']
35
+ builder.xcode.get_build_configurations # ['Debug', 'Release', 'Test']
36
+ builder.xcode.get_target # ['Target 1', 'Target 2']
37
+ ```
@@ -0,0 +1,78 @@
1
+ require 'fruity_builder/lib/sys_log'
2
+ require 'fruity_builder/xcodebuild'
3
+ require 'fruity_builder/build_properties'
4
+
5
+
6
+ module FruityBuilder
7
+ module IOS
8
+
9
+ attr_accessor :log
10
+
11
+ @@log = FruityBuilder::SysLog.new
12
+
13
+ def self.log
14
+ @@log
15
+ end
16
+
17
+ def self.set_logger(log)
18
+ @@log = log
19
+ end
20
+
21
+ class Helper
22
+ attr_accessor :path, :project, :workspace, :build, :plist, :xcode
23
+
24
+ def initialize(path)
25
+ @path = path
26
+ end
27
+
28
+ def project
29
+ if @project.nil?
30
+ if @path.scan(/.*xcodeproj$/).count > 0
31
+ return @path
32
+ end
33
+ projects = Dir["#{@path}/**/*.xcodeproj"]
34
+ projects = projects.select { |project| !project.include?('Pods')}
35
+ raise HelperCommandError.new('Project not found') if projects.empty?
36
+ raise HelperCommandError.new('Mulitple projects found, please specify one') if projects.count > 1
37
+ project = projects.first
38
+ @project = project
39
+ end
40
+ @project
41
+ end
42
+
43
+ def workspace
44
+ if @workspace.nil?
45
+ if @path.scan(/.*xcworkspace$/).count > 0
46
+ return @path
47
+ end
48
+ workspace = Dir["#{@path}/**/*.xcworkspace"].first
49
+ raise 'Workspace not found' if workspace.nil?
50
+ @workspace = workspace
51
+ end
52
+ @workspace
53
+ end
54
+
55
+ # Handle for the BuildProperties class
56
+ def build
57
+ if @build.nil?
58
+ @build = FruityBuilder::IOS::BuildProperties.new("#{project}/project.pbxproj")
59
+ end
60
+ @build
61
+ end
62
+
63
+ # Handle for the XCodeBuild class
64
+ def xcode
65
+ if @xcode.nil?
66
+ @xcode = FruityBuilder::IOS::XCodeBuild.new(project)
67
+ end
68
+ @xcode
69
+ end
70
+ end
71
+ end
72
+
73
+ class HelperCommandError < StandardError
74
+ def initialize(msg)
75
+ super(msg)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,82 @@
1
+ require 'fruity_builder/lib/execution'
2
+ require 'fruity_builder/plistutil'
3
+
4
+ module FruityBuilder
5
+ module IOS
6
+ class BuildProperties < Execution
7
+
8
+ attr_accessor :project, :properties
9
+
10
+ def initialize(project_path)
11
+ @project = project_path
12
+ end
13
+
14
+ def open_project_properties
15
+ @properties = File.read(@project)
16
+ end
17
+
18
+ def properties
19
+ open_project_properties if @properties.nil?
20
+ @properties
21
+ end
22
+
23
+ def save_project_properties
24
+ File.write(@project, properties)
25
+ end
26
+
27
+ def replace_bundle_id(new_bundle_id)
28
+ path = Pathname.new(File.dirname(@project) + '/../').realdirpath.to_s
29
+ xcode = FruityBuilder::IOS::XCodeBuild.new(File.dirname(@project))
30
+ targets = xcode.get_targets
31
+ project_files = Dir["#{path}/**/Info.plist"]
32
+
33
+ files = project_files.select { |project| targets.any? { |target| project.include?("#{target}/Info.plist") } }
34
+ files.each do |file|
35
+ FruityBuilder::IOS::Plistutil.replace_bundle_id(new_id: new_bundle_id, file: file)
36
+ end
37
+ end
38
+
39
+ def get_dev_teams
40
+ @properties.scan(/.*DevelopmentTeam = (.*);.*/).uniq.flatten
41
+ end
42
+
43
+ def get_code_signing_identities
44
+ @properties.scan(/.*CODE_SIGN_IDENTITY.*= "(.*)";.*/).uniq.flatten
45
+ end
46
+
47
+ def get_provisioning_profiles
48
+ @properties.scan(/.*PROVISIONING_PROFILE = "(.*)";.*/).uniq.flatten
49
+ end
50
+
51
+ def replace_dev_team(new_dev_team)
52
+ @properties = self.class.replace_project_data(regex: '.*DevelopmentTeam = (.*);.*', data: properties, new_value: new_dev_team)
53
+ end
54
+
55
+ def replace_code_sign_identity(new_identity)
56
+ @properties = self.class.replace_project_data(regex: '.*CODE_SIGN_IDENTITY.*= "(.*)";.*', data: properties, new_value: new_identity)
57
+ end
58
+
59
+ def replace_provisioning_profile(new_profile)
60
+ @properties = self.class.replace_project_data(regex: '.*PROVISIONING_PROFILE = "(.*)";.*', data: properties, new_value: new_profile)
61
+ end
62
+
63
+ def self.replace_project_data(options = {})
64
+ regex = Regexp.new(options[:regex])
65
+ replacements = options[:data].scan(regex).uniq.flatten
66
+
67
+ result = options[:data]
68
+ replacements.each do |to_replace|
69
+ result = result.gsub(to_replace, options[:new_value])
70
+ end
71
+
72
+ result
73
+ end
74
+ end
75
+
76
+ class BuildPropertiesError < StandardError
77
+ def initialize(msg)
78
+ super(msg)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,78 @@
1
+ # Encoding: utf-8
2
+ require 'open3'
3
+ require 'ostruct'
4
+ require 'timeout'
5
+ #require 'fruity_builder'
6
+
7
+ module FruityBuilder
8
+ # Provides method to execute terminal commands in a reusable way
9
+ class Execution
10
+
11
+ # execute_with_timeout_and_retry constants
12
+ COMMAND_TIMEOUT = 30 # base number of seconds to wait until adb command times out
13
+ COMMAND_RETRIES = 5 # number of times we will retry the adb command.
14
+ # actual maximum seconds waited before timeout is
15
+ # (1 * s) + (2 * s) + (3 * s) ... up to (n * s)
16
+ # where s = COMMAND_TIMEOUT
17
+ # n = COMMAND_RETRIES
18
+
19
+ def self.execute(command)
20
+ # Execute out to shell
21
+ # Returns a struct collecting the execution results
22
+ # struct = DeviceAPI::ADB.execute( 'adb devices' )
23
+ # struct.stdout #=> "std out"
24
+ # struct.stderr #=> ''
25
+ # strict.exit #=> 0
26
+ result = OpenStruct.new
27
+
28
+ stdout, stderr, status = Open3.capture3(command)
29
+
30
+ result.exit = status.exitstatus
31
+ result.stdout = stdout
32
+ result.stderr = stderr
33
+
34
+ result
35
+ end
36
+
37
+ def self.execute_with_timeout_and_retry(command)
38
+ retries_left = COMMAND_RETRIES
39
+ cmd_successful = false
40
+ result = 0
41
+
42
+ while (retries_left > 0) and (cmd_successful == false) do
43
+ begin
44
+ ::Timeout.timeout(COMMAND_TIMEOUT) do
45
+ result = execute(command)
46
+ cmd_successful = true
47
+ end
48
+ rescue ::Timeout::Error
49
+ retries_left -= 1
50
+ if retries_left > 0
51
+ FruityBuilder.log.error "Command #{command} timed out after #{COMMAND_TIMEOUT.to_s} sec, retrying,"\
52
+ + " #{retries_left.to_s} attempts left.."
53
+ end
54
+ end
55
+ end
56
+
57
+ if retries_left < COMMAND_RETRIES # if we had to retry
58
+ if cmd_successful == false
59
+ msg = "Command #{command} timed out after #{COMMAND_RETRIES.to_s} retries. !"\
60
+ + " Exiting.."
61
+ FruityBuilder.log.fatal(msg)
62
+ raise FruityBuilder::CommandTimeoutError.new(msg)
63
+ else
64
+ FruityBuilder.log.info "Command #{command} succeeded execution after retrying"
65
+ end
66
+ end
67
+
68
+ result
69
+ end
70
+ end
71
+
72
+ class CommandTimeoutError < StandardError
73
+ def initialize(msg)
74
+ super(msg)
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,39 @@
1
+ require 'syslog'
2
+
3
+ module FruityBuilder
4
+ class SysLog
5
+
6
+ attr_accessor :syslog
7
+
8
+ def log(priority,message)
9
+ if @syslog and @syslog.opened?
10
+ @syslog = Syslog.reopen('device-api-gem', Syslog::LOG_PID, Syslog::LOG_DAEMON)
11
+ else
12
+ @syslog = Syslog.open('device-api-gem', Syslog::LOG_PID, Syslog::LOG_DAEMON)
13
+ end
14
+
15
+ @syslog.log(priority,message)
16
+ @syslog.close
17
+ end
18
+
19
+ def fatal(message)
20
+ self.log(Syslog::LOG_CRIT,message)
21
+ end
22
+
23
+ def error(message)
24
+ self.log(Syslog::LOG_ERR,message)
25
+ end
26
+
27
+ def warn(message)
28
+ self.log(Syslog::LOG_WARNING,message)
29
+ end
30
+
31
+ def info(message)
32
+ self.log(Syslog::LOG_INFO,message)
33
+ end
34
+
35
+ def debug(message)
36
+ self.log(Syslog::LOG_DEBUG,message)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ require 'fruity_builder/lib/execution'
2
+
3
+ module FruityBuilder
4
+ module IOS
5
+ class Plistutil < Execution
6
+
7
+ def self.get_bundle_id(options = {})
8
+ if options.key?(:file)
9
+ xml = IO.read(options[:file])
10
+ elsif options.key?(:xml)
11
+ xml = options[:xml]
12
+ end
13
+
14
+ raise PlistutilCommandError.new('No XML was passed') unless xml
15
+
16
+ identifiers = xml.scan(/.*CFBundleIdentifier<\/key>\n\t<string>(.*?)<\/string>/)
17
+ identifiers << xml.scan(/.*CFBundleName<\/key>\n\t<string>(.*?)<\/string>/)
18
+
19
+ identifiers.flatten.uniq
20
+ end
21
+
22
+ def self.replace_bundle_id(options = {})
23
+ if options.key?(:file)
24
+ xml = IO.read(options[:file])
25
+ elsif options.key?(:xml)
26
+ xml = options[:xml]
27
+ end
28
+
29
+ raise PlistutilCommandError.new('No XML was passed') unless xml
30
+
31
+ replacements = xml.scan(/.*CFBundleIdentifier<\/key>\n\t<string>(.*?)<\/string>/)
32
+ replacements << xml.scan(/.*CFBundleName<\/key>\n\t<string>(.*?)<\/string>/)
33
+
34
+ replacements.flatten.uniq.each do |replacement|
35
+ xml = xml.gsub(replacement, options[:new_id])
36
+ end
37
+
38
+ IO.write(options[:file], xml) if options.key?(:file)
39
+ xml
40
+ end
41
+ end
42
+
43
+ # plistutil error class
44
+ class PlistutilCommandError < StandardError
45
+ def initialize(msg)
46
+ super(msg)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,60 @@
1
+ require 'fruity_builder/lib/execution'
2
+
3
+ module FruityBuilder
4
+ module IOS
5
+ class XCodeBuild < Execution
6
+
7
+ attr_accessor :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def get_schemes
14
+ self.class.get_schemes(path)
15
+ end
16
+
17
+ def get_targets
18
+ self.class.get_targets(path)
19
+ end
20
+
21
+ def get_build_configurations
22
+ self.class.get_build_configurations(path)
23
+ end
24
+
25
+ def self.retrieve_project_section(project_info, section)
26
+ index = project_info.index(section)
27
+ section_values = []
28
+ for i in index+1..project_info.count - 1
29
+ break if project_info[i].empty?
30
+ section_values << project_info[i]
31
+ end
32
+ section_values
33
+ end
34
+
35
+ def self.get_schemes(project_path)
36
+ retrieve_project_section(get_project_info(project_path), 'Schemes:')
37
+ end
38
+
39
+ def self.get_build_configurations(project_path)
40
+ retrieve_project_section(get_project_info(project_path), 'Build Configurations:')
41
+ end
42
+
43
+ def self.get_targets(project_path)
44
+ retrieve_project_section(get_project_info(project_path), 'Targets:')
45
+ end
46
+
47
+ def self.get_project_info(project_path)
48
+ info = execute("xcodebuild -project #{project_path} -list")
49
+ raise XCodeBuildCommandError.new(info.stderr) if info.exit != 0
50
+ info.stdout.split("\n").map { |a| a.strip }
51
+ end
52
+ end
53
+
54
+ class XCodeBuildCommandError < StandardError
55
+ def initialize(msg)
56
+ super(msg)
57
+ end
58
+ end
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fruity_builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - BBC
8
+ - Jon Wilson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-10-20 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: iOS code signing utilities - used to replace bundle IDs, development
15
+ teams and provisioning profiles programmatically
16
+ email:
17
+ - jon.wilson01@bbc.co.uk
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - README.md
23
+ - lib/fruity_builder.rb
24
+ - lib/fruity_builder/build_properties.rb
25
+ - lib/fruity_builder/lib/execution.rb
26
+ - lib/fruity_builder/lib/sys_log.rb
27
+ - lib/fruity_builder/plistutil.rb
28
+ - lib/fruity_builder/xcodebuild.rb
29
+ homepage: https://github.com/bbc/fruity_builder
30
+ licenses:
31
+ - MIT
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.4.8
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: iOS code signing utilities
53
+ test_files: []