fruity_builder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []