fruity_builder 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/lib/fruity_builder.rb +78 -0
- data/lib/fruity_builder/build_properties.rb +82 -0
- data/lib/fruity_builder/lib/execution.rb +78 -0
- data/lib/fruity_builder/lib/sys_log.rb +39 -0
- data/lib/fruity_builder/plistutil.rb +50 -0
- data/lib/fruity_builder/xcodebuild.rb +60 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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: []
|