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.
- 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: []
|