branch_io_cli 0.1.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: 1ccbe6055b11c7d84271ce19783c90890e6c7b5f
4
+ data.tar.gz: ac6119e721f6e4c8529a2a01f4fc76fbb97e096e
5
+ SHA512:
6
+ metadata.gz: f8e945dc2c6db1e08264537075bec5f45f8c562e58a3bc37821ce1ab7c1493021f95b9a89c25d08dfcac6ef7fee7a4f3a0904536967bb6cd961fd17416746f32
7
+ data.tar.gz: 804615d6d37b159d79e3b801a4d818f62ffebfe45d50c60160253ca7469074c939806a3d64f35a7a7f9dbd9fa69b9b3418b324b7bba8c6802ffc9dd91e56f66e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Branch Metrics, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,121 @@
1
+ # branch_io_cli gem
2
+
3
+ This is a command-line tool to integrate the Branch SDK into mobile app projects. (Currently iOS only.)
4
+
5
+ [![Gem](https://img.shields.io/gem/v/branch_io_cli.svg?style=flat)](https://rubygems.org/gems/branch_io_cli)
6
+ [![Downloads](https://img.shields.io/gem/dt/branch_io_cli.svg?style=flat)](https://rubygems.org/gems/branch_io_cli)
7
+ [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/BranchMetrics/branch_io_cli/blob/master/LICENSE)
8
+ [![CircleCI](https://img.shields.io/circleci/project/github/BranchMetrics/branch_io_cli.svg)](https://circleci.com/gh/BranchMetrics/branch_io_cli)
9
+
10
+ ## Preliminary release
11
+
12
+ This is a preliminary release of this gem. Please report any problems by opening issues in this repo.
13
+
14
+ ## Getting started
15
+
16
+ ```bash
17
+ gem install branch_io_cli
18
+ ```
19
+
20
+ Note that this command may require `sudo` access if you are using the system Ruby, i.e. `sudo gem install branch_io_cli`.
21
+
22
+ ```bash
23
+ branch_io -h
24
+ branch_io setup -h
25
+ branch_io validate -h
26
+ ```
27
+
28
+ ## Commands
29
+
30
+ ### Setup command
31
+
32
+ ```bash
33
+ branch_io setup
34
+ ```
35
+
36
+ Integrates the Branch SDK into a native app project. This currently supports iOS only.
37
+ It will infer the project location if there is exactly one .xcodeproj anywhere under
38
+ the current directory, excluding any in a Pods or Carthage folder. Otherwise, specify
39
+ the project location using the `--xcodeproj` option.
40
+
41
+ If a Podfile or Cartfile is detected, the Branch SDK will be added to the relevant
42
+ configuration file and the dependencies updated to include the Branch framework.
43
+ This behavior may be suppressed using `--no_add_sdk`. If no Podfile or Cartfile
44
+ is found, the SDK dependency must be added manually. This will improve in a future
45
+ release.
46
+
47
+ By default, all supplied Universal Link domains are validated. If validation passes,
48
+ the setup continues. If validation fails, no further action is taken. Suppress
49
+ validation using `--no_validate` or force changes when validation fails using
50
+ `--force`.
51
+
52
+ All relevant project settings are modified. The Branch keys are added to the Info.plist,
53
+ along with the `branch_universal_link_domains` key for custom domains (when `--domains`
54
+ is used). All domains are added to the project's Associated Domains entitlements.
55
+ An entitlements file is added if none is found. Optionally, if `--frameworks` is
56
+ specified, this command can add a list of system frameworks to the project (e.g.,
57
+ AdSupport, CoreSpotlight, SafariServices).
58
+
59
+ A language-specific patch is applied to the AppDelegate (Swift or Objective-C).
60
+ This can be suppressed using `--no_patch_source`.
61
+
62
+ #### Prerequisites
63
+
64
+ Before using this action, make sure to set up your app in the [Branch Dashboard](https://dashboard.branch.io). See https://docs.branch.io/pages/dashboard/integrate/ for details. To use the `setup` command, you need:
65
+
66
+ - Branch key(s), either live, test or both
67
+ - Domain name(s) used for Branch links
68
+ - Location of your Xcode project
69
+
70
+ #### Options
71
+
72
+ |Option|Description|
73
+ |------|-----------|
74
+ |--live_key key_live_xxxx|Branch live key|
75
+ |--test_key key_test_yyyy|Branch test key|
76
+ |--app_link_subdomain myapp|Branch app.link subdomain, e.g. myapp for myapp.app.link|
77
+ |--domains example.com,www.example.com|Comma-separated list of custom domain(s) or non-Branch domain(s)|
78
+ |--xcodeproj MyProject.xcodeproj|Path to an Xcode project to update|
79
+ |--target MyAppTarget|Name of a target to modify in the Xcode project|
80
+ |--podfile /path/to/Podfile|Path to the Podfile for the project|
81
+ |--cartfile /path/to/Cartfile|Path to the Cartfile for the project|
82
+ |--frameworks AdSupport,CoreSpotlight,SafariServices|Comma-separated list of system frameworks to add to the project|
83
+ |--no_pod_repo_update|Skip update of the local podspec repo before installing|
84
+ |--no_validate|Skip validation of Universal Link configuration|
85
+ |--force|Update project even if Universal Link validation fails|
86
+ |--no_add_sdk|Don't add the Branch framework to the project|
87
+ |--no_patch_source|Don't add Branch SDK calls to the AppDelegate|
88
+ |--commit|Commit the results to Git|
89
+
90
+ All parameters are optional, but either `--live_key` or `--test_key` or both must be specified, as well as
91
+ `--app_link_subdomain` or `--domains` or both.
92
+
93
+ ### Validate command
94
+
95
+ ```bash
96
+ branch_io validate
97
+ ```
98
+
99
+ This command validates all Universal Link domains configured in a project without making any modification.
100
+ It validates both Branch and non-Branch domains. Unlike web-based Universal Link validators,
101
+ this command operates directly on the project. It finds the bundle and
102
+ signing team identifiers in the project as well as the app's Associated Domains.
103
+ It requests the apple-app-site-association file for each domain and validates
104
+ the file against the project's settings.
105
+
106
+ #### Options
107
+
108
+ |Option|Description|
109
+ |------|-----------|
110
+ |--domains example.com,www.example.com|Comma-separated list of domains. May include app.link subdomains.|
111
+ |--xcodeproj MyProject.xcodeproj|Path to an Xcode project to update|
112
+ |--target MyAppTarget|Name of a target to modify in the Xcode project|
113
+
114
+ All parameters are optional. If `--domains` is specified, the list of Universal Link domains in the
115
+ Associated Domains entitlement must exactly match this list, without regard to order. If no `--domains`
116
+ are provided, validation passes if at least one Universal Link domain is configured and passes validation,
117
+ and no Universal Link domain is present that does not pass validation.
118
+
119
+ #### Return value
120
+
121
+ If validation passes, this command returns 0. If validation fails, it returns 1.
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "branch_io_cli"
4
+
5
+ BranchIOCLI::CLI.new.run
@@ -0,0 +1,4 @@
1
+ require "branch_io_cli/cli"
2
+ require "branch_io_cli/command"
3
+ require "branch_io_cli/helper"
4
+ require "branch_io_cli/version"
@@ -0,0 +1,59 @@
1
+ require "rubygems"
2
+ require "commander"
3
+
4
+ module BranchIOCLI
5
+ class CLI
6
+ include Commander::Methods
7
+
8
+ def run
9
+ program :name, "Branch.io command-line interface"
10
+ program :version, VERSION
11
+ program :description, "More to come"
12
+
13
+ command :setup do |c|
14
+ c.syntax = "branch_io setup"
15
+ c.description = "Set up an iOS project to use the Branch SDK."
16
+
17
+ # Required Branch params
18
+ c.option "--live_key key_live_xxxx", String, "Branch live key"
19
+ c.option "--test_key key_test_yyyy", String, "Branch test key"
20
+ c.option "--app_link_subdomain myapp", String, "Branch app.link subdomain, e.g. myapp for myapp.app.link"
21
+ c.option "--domains example.com,www.example.com", Array, "Comma-separated list of custom domain(s) or non-Branch domain(s)"
22
+
23
+ c.option "--xcodeproj MyProject.xcodeproj", String, "Path to an Xcode project to update"
24
+ c.option "--target MyAppTarget", String, "Name of a target to modify in the Xcode project"
25
+ c.option "--podfile /path/to/Podfile", String, "Path to the Podfile for the project"
26
+ c.option "--cartfile /path/to/Cartfile", String, "Path to the Cartfile for the project"
27
+ c.option "--frameworks AdSupport,CoreSpotlight,SafariServices", Array, "Comma-separated list of system frameworks to add to the project"
28
+
29
+ c.option "--no_pod_repo_update", TrueClass, "Skip update of the local podspec repo before installing"
30
+ c.option "--no_validate", TrueClass, "Skip validation of Universal Link configuration"
31
+ c.option "--force", TrueClass, "Update project even if Universal Link validation fails"
32
+ c.option "--no_add_sdk", TrueClass, "Don't add the Branch framework to the project"
33
+ c.option "--no_patch_source", TrueClass, "Don't add Branch SDK calls to the AppDelegate"
34
+ c.option "--commit", TrueClass, "Commit the results to Git"
35
+
36
+ c.action do |args, options|
37
+ Command.setup options
38
+ end
39
+ end
40
+
41
+ command :validate do |c|
42
+ c.syntax = "branch_io validate"
43
+ c.description = "Validate the Universal Link configuration for an Xcode project"
44
+
45
+ c.option "--xcodeproj MyProject.xcodeproj", String, "Path to an Xcode project to update"
46
+ c.option "--target MyAppTarget", String, "Name of a target to modify in the Xcode project"
47
+ c.option "--domains example.com,www.example.com", Array, "Comma-separated list of domains to validate (Branch domains or non-Branch domains)"
48
+
49
+ c.action do |args, options|
50
+ valid = Command.validate options
51
+ exit_code = valid ? 0 : 1
52
+ exit exit_code
53
+ end
54
+ end
55
+
56
+ run!
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,281 @@
1
+ require "xcodeproj"
2
+
3
+ module BranchIOCLI
4
+ class Command
5
+ class << self
6
+ def setup(options)
7
+ @domains = all_domains options
8
+ @keys = keys options
9
+
10
+ if @keys.empty?
11
+ say "Please specify --live_key or --test_key or both."
12
+ return
13
+ end
14
+
15
+ if @domains.empty?
16
+ say "Please specify --app_link_subdomain or --domains or both."
17
+ return
18
+ end
19
+
20
+ @xcodeproj_path = xcodeproj_path options
21
+ unless @xcodeproj_path
22
+ say "Please specify the --xcodeproj option."
23
+ return
24
+ end
25
+
26
+ # raises
27
+ xcodeproj = Xcodeproj::Project.open @xcodeproj_path
28
+
29
+ update_podfile(options) || update_cartfile(options, xcodeproj)
30
+
31
+ target = options.target # may be nil
32
+
33
+ if !options.no_validate &&
34
+ !helper.validate_team_and_bundle_ids_from_aasa_files(xcodeproj, target, @domains)
35
+ say "Universal Link configuration failed validation."
36
+ helper.errors.each { |error| say " #{error}" }
37
+ return unless options.force
38
+ elsif !options.no_validate
39
+ say "Universal Link configuration passed validation. ✅"
40
+ end
41
+
42
+ # the following calls can all raise IOError
43
+ helper.add_keys_to_info_plist xcodeproj, target, @keys
44
+ helper.add_branch_universal_link_domains_to_info_plist xcodeproj, target, @domains
45
+ new_path = helper.add_universal_links_to_project xcodeproj, target, @domains, false
46
+ `git add #{new_path}` if options.commit && new_path
47
+
48
+ helper.add_system_frameworks xcodeproj, target, options.frameworks unless options.frameworks.nil? || options.frameworks.empty?
49
+
50
+ xcodeproj.save
51
+
52
+ patch_source xcodeproj unless options.no_patch_source
53
+
54
+ return unless options.commit
55
+
56
+ `git commit #{helper.changes.join(" ")} -m '[branch_io_cli] Branch SDK integration'`
57
+ end
58
+
59
+ def validate(options)
60
+ path = xcodeproj_path options
61
+ unless path
62
+ say "Please specify the --xcodeproj option."
63
+ return
64
+ end
65
+
66
+ # raises
67
+ xcodeproj = Xcodeproj::Project.open path
68
+
69
+ valid = true
70
+
71
+ unless options.domains.nil? || options.domains.empty?
72
+ domains_valid = helper.validate_project_domains(
73
+ options.domains,
74
+ xcodeproj,
75
+ options.target
76
+ )
77
+
78
+ if domains_valid
79
+ say "Project domains match :domains parameter: ✅"
80
+ else
81
+ say "Project domains do not match specified :domains"
82
+ helper.errors.each { |error| say " #{error}" }
83
+ end
84
+
85
+ valid &&= domains_valid
86
+ end
87
+
88
+ configuration_valid = helper.validate_team_and_bundle_ids_from_aasa_files xcodeproj, options.target
89
+ unless configuration_valid
90
+ say "Universal Link configuration failed validation."
91
+ helper.errors.each { |error| say " #{error}" }
92
+ end
93
+
94
+ valid &&= configuration_valid
95
+
96
+ say "Universal Link configuration passed validation. ✅" if valid
97
+
98
+ valid
99
+ end
100
+
101
+ def helper
102
+ BranchIOCLI::Helper::BranchHelper
103
+ end
104
+
105
+ def xcodeproj_path(options)
106
+ return options.xcodeproj if options.xcodeproj
107
+
108
+ repo_path = "."
109
+
110
+ all_xcodeproj_paths = Dir[File.expand_path(File.join(repo_path, '**/*.xcodeproj'))]
111
+ # find an xcodeproj (ignoring the Pods and Carthage folders)
112
+ # TODO: Improve this filter
113
+ xcodeproj_paths = all_xcodeproj_paths.reject { |p| p =~ /Pods|Carthage/ }
114
+
115
+ # no projects found: error
116
+ say 'Could not find a .xcodeproj in the current repository\'s working directory.' and return nil if xcodeproj_paths.count == 0
117
+
118
+ # too many projects found: error
119
+ if xcodeproj_paths.count > 1
120
+ repo_pathname = Pathname.new repo_path
121
+ relative_projects = xcodeproj_paths.map { |e| Pathname.new(e).relative_path_from(repo_pathname).to_s }.join("\n")
122
+ say "Found multiple .xcodeproj projects in the current repository's working directory. Please specify your app's main project: \n#{relative_projects}"
123
+ return nil
124
+ end
125
+
126
+ # one project found: great
127
+ xcodeproj_paths.first
128
+ end
129
+
130
+ def app_link_subdomains(options)
131
+ app_link_subdomain = options.app_link_subdomain
132
+ live_key = options.live_key
133
+ test_key = options.test_key
134
+ return [] if live_key.nil? and test_key.nil?
135
+ return [] if app_link_subdomain.nil?
136
+
137
+ domains = []
138
+ unless live_key.nil?
139
+ domains += [
140
+ "#{app_link_subdomain}.app.link",
141
+ "#{app_link_subdomain}-alternate.app.link"
142
+ ]
143
+ end
144
+ unless test_key.nil?
145
+ domains += [
146
+ "#{app_link_subdomain}.test-app.link",
147
+ "#{app_link_subdomain}-alternate.test-app.link"
148
+ ]
149
+ end
150
+ domains
151
+ end
152
+
153
+ def all_domains(options)
154
+ app_link_subdomains = app_link_subdomains options
155
+ custom_domains = options.domains || []
156
+ (app_link_subdomains + custom_domains).uniq
157
+ end
158
+
159
+ def keys(options)
160
+ live_key = options.live_key
161
+ test_key = options.test_key
162
+ keys = {}
163
+ keys[:live] = live_key unless live_key.nil?
164
+ keys[:test] = test_key unless test_key.nil?
165
+ keys
166
+ end
167
+
168
+ def podfile_path(options)
169
+ # Disable Podfile update if add_sdk: false is present
170
+ return nil if options.no_add_sdk
171
+
172
+ # Use the :podfile parameter if present
173
+ if options.podfile
174
+ raise "--podfile argument must specify a path ending in '/Podfile'" unless options.podfile =~ %r{/Podfile$}
175
+ podfile_path = File.expand_path options.podfile, "."
176
+ return podfile_path if File.exist? podfile_path
177
+ raise "#{podfile_path} not found"
178
+ end
179
+
180
+ # Look in the same directory as the project (typical setup)
181
+ podfile_path = File.expand_path "../Podfile", @xcodeproj_path
182
+ return podfile_path if File.exist? podfile_path
183
+ end
184
+
185
+ def cartfile_path(options)
186
+ # Disable Cartfile update if add_sdk: false is present
187
+ return nil if options.no_add_sdk
188
+
189
+ # Use the :cartfile parameter if present
190
+ if options.cartfile
191
+ raise "--cartfile argument must specify a path ending in '/Cartfile'" unless options.cartfile =~ %r{/Cartfile$}
192
+ cartfile_path = File.expand_path options.cartfile, "."
193
+ return cartfile_path if File.exist? cartfile_path
194
+ raise "#{cartfile_path} not found"
195
+ end
196
+
197
+ # Look in the same directory as the project (typical setup)
198
+ cartfile_path = File.expand_path "../Cartfile", @xcodeproj_path
199
+ return cartfile_path if File.exist? cartfile_path
200
+ end
201
+
202
+ def update_podfile(options)
203
+ podfile_path = podfile_path options
204
+ return false if podfile_path.nil?
205
+
206
+ # 1. Patch Podfile. Return if no change (Branch pod already present).
207
+ return false unless helper.patch_podfile podfile_path
208
+
209
+ # 2. pod install
210
+ # command = "PATH='#{ENV['PATH']}' pod install"
211
+ command = 'pod install'
212
+ command += ' --repo-update' unless options.no_pod_repo_update
213
+
214
+ Dir.chdir(File.dirname(podfile_path)) do
215
+ `#{command}`
216
+ end
217
+
218
+ # 3. Add Podfile and Podfile.lock to commit (in case :commit param specified)
219
+ helper.add_change podfile_path
220
+ helper.add_change "#{podfile_path}.lock"
221
+
222
+ # 4. Check if Pods folder is under SCM
223
+ pods_folder_path = File.expand_path "../Pods", podfile_path
224
+ `git ls-files #{pods_folder_path} --error-unmatch > /dev/null 2>&1`
225
+ return true unless $?.exitstatus == 0
226
+
227
+ # 5. If so, add the Pods folder to the commit (in case :commit param specified)
228
+ helper.add_change pods_folder_path
229
+ other_action.git_add path: pods_folder_path if options.commit
230
+ true
231
+ end
232
+
233
+ def update_cartfile(options, project)
234
+ cartfile_path = cartfile_path options
235
+ return false if cartfile_path.nil?
236
+
237
+ # 1. Patch Cartfile. Return if no change (Branch already present).
238
+ return false unless helper.patch_cartfile cartfile_path
239
+
240
+ # 2. carthage update
241
+ Dir.chdir(File.dirname(cartfile_path)) do
242
+ `carthage update`
243
+ end
244
+
245
+ # 3. Add Cartfile and Cartfile.resolved to commit (in case :commit param specified)
246
+ helper.add_change cartfile_path
247
+ helper.add_change "#{cartfile_path}.resolved"
248
+
249
+ # 4. Add to target depependencies
250
+ frameworks_group = project['Frameworks']
251
+ branch_framework = frameworks_group.new_file "Carthage/Build/iOS/Branch.framework"
252
+ target = helper.target_from_project project, options.target
253
+ target.frameworks_build_phase.add_file_reference branch_framework
254
+
255
+ # 5. Add to copy-frameworks build phase
256
+ carthage_build_phase = target.build_phases.find do |phase|
257
+ phase.respond_to?(:shell_script) && phase.shell_script =~ /carthage\s+copy-frameworks/
258
+ end
259
+
260
+ if carthage_build_phase
261
+ carthage_build_phase.input_paths << "$(SRCROOT)/Carthage/Build/iOS/Branch.framework"
262
+ carthage_build_phase.output_paths << "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Branch.framework"
263
+ end
264
+
265
+ # 6. Check if Carthage folder is under SCM
266
+ carthage_folder_path = File.expand_path "../Carthage", cartfile_path
267
+ `git ls-files #{carthage_folder_path} --error-unmatch > /dev/null 2>&1`
268
+ return true unless $?.exitstatus == 0
269
+
270
+ # 7. If so, add the Pods folder to the commit (in case :commit param specified)
271
+ helper.add_change carthage_folder_path
272
+ other_action.git_add path: carthage_folder_path if options.commit
273
+ true
274
+ end
275
+
276
+ def patch_source(xcodeproj)
277
+ helper.patch_app_delegate_swift(xcodeproj) || helper.patch_app_delegate_objc(xcodeproj)
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1 @@
1
+ require "branch_io_cli/helper/branch_helper"
@@ -0,0 +1,86 @@
1
+ module BranchIOCLI
2
+ module Helper
3
+ module AndroidHelper
4
+ def add_keys_to_android_manifest(manifest, keys)
5
+ add_metadata_to_manifest manifest, "io.branch.sdk.BranchKey", keys[:live] unless keys[:live].nil?
6
+ add_metadata_to_manifest manifest, "io.branch.sdk.BranchKey.test", keys[:test] unless keys[:test].nil?
7
+ end
8
+
9
+ # TODO: Work on all XML/AndroidManifest formatting
10
+
11
+ def add_metadata_to_manifest(manifest, key, value)
12
+ element = manifest.elements["//manifest/application/meta-data[@android:name=\"#{key}\"]"]
13
+ if element.nil?
14
+ application = manifest.elements["//manifest/application"]
15
+ application.add_element "meta-data", "android:name" => key, "android:value" => value
16
+ else
17
+ element.attributes["android:value"] = value
18
+ end
19
+ end
20
+
21
+ def add_intent_filters_to_android_manifest(manifest, domains, uri_scheme, activity_name, remove_existing)
22
+ if activity_name
23
+ activity = manifest.elements["//manifest/application/activity[@android:name=\"#{activity_name}\""]
24
+ else
25
+ activity = find_activity manifest
26
+ end
27
+
28
+ raise "Failed to find an Activity in the Android manifest" if activity.nil?
29
+
30
+ if remove_existing
31
+ remove_existing_domains(activity)
32
+ end
33
+
34
+ add_intent_filter_to_activity activity, domains, uri_scheme
35
+ end
36
+
37
+ def find_activity(manifest)
38
+ # try to infer the right activity
39
+ # look for the first singleTask
40
+ single_task_activity = manifest.elements["//manifest/application/activity[@android:launchMode=\"singleTask\"]"]
41
+ return single_task_activity if single_task_activity
42
+
43
+ # no singleTask activities. Take the first Activity
44
+ # TODO: Add singleTask?
45
+ manifest.elements["//manifest/application/activity"]
46
+ end
47
+
48
+ def add_intent_filter_to_activity(activity, domains, uri_scheme)
49
+ # Add a single intent-filter with autoVerify and a data element for each domain and the optional uri_scheme
50
+ intent_filter = REXML::Element.new "intent-filter"
51
+ intent_filter.attributes["android:autoVerify"] = true
52
+ intent_filter.add_element "action", "android:name" => "android.intent.action.VIEW"
53
+ intent_filter.add_element "category", "android:name" => "android.intent.category.DEFAULT"
54
+ intent_filter.add_element "category", "android:name" => "android.intent.category.BROWSABLE"
55
+ intent_filter.elements << uri_scheme_data_element(uri_scheme) unless uri_scheme.nil?
56
+ app_link_data_elements(domains).each { |e| intent_filter.elements << e }
57
+
58
+ activity.add_element intent_filter
59
+ end
60
+
61
+ def remove_existing_domains(activity)
62
+ # Find all intent-filters that include a data element with android:scheme
63
+ # TODO: Can this be done with a single css/at_css call?
64
+ activity.elements.each("//manifest//intent-filter") do |filter|
65
+ filter.remove if filter.elements["data[@android:scheme]"]
66
+ end
67
+ end
68
+
69
+ def app_link_data_elements(domains)
70
+ domains.map do |domain|
71
+ element = REXML::Element.new "data"
72
+ element.attributes["android:scheme"] = "https"
73
+ element.attributes["android:host"] = domain
74
+ element
75
+ end
76
+ end
77
+
78
+ def uri_scheme_data_element(uri_scheme)
79
+ element = REXML::Element.new "data"
80
+ element.attributes["android:scheme"] = uri_scheme
81
+ element.attributes["android:host"] = "open"
82
+ element
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,39 @@
1
+ require "branch_io_cli/helper/android_helper"
2
+ require "branch_io_cli/helper/ios_helper"
3
+ require "pattern_patch"
4
+ require "set"
5
+
6
+ module BranchIOCLI
7
+ module Helper
8
+ class BranchHelper
9
+ class << self
10
+ attr_accessor :changes # An array of file paths (Strings) that were modified
11
+ attr_accessor :errors # An array of error messages (Strings) from validation
12
+
13
+ include AndroidHelper
14
+ include IOSHelper
15
+
16
+ def add_change(change)
17
+ @changes ||= Set.new
18
+ @changes << change.to_s
19
+ end
20
+
21
+ # Shim around PatternPatch for now
22
+ def apply_patch(options)
23
+ modified = File.open(options[:files]) do |file|
24
+ PatternPatch::Utilities.apply_patch file.read,
25
+ options[:regexp],
26
+ options[:text],
27
+ options[:global],
28
+ options[:mode],
29
+ options[:offset] || 0
30
+ end
31
+
32
+ File.open(options[:files], "w") do |file|
33
+ file.write modified
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,499 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "openssl"
4
+ require "plist"
5
+
6
+ module BranchIOCLI
7
+ module Helper
8
+ module IOSHelper
9
+ APPLINKS = "applinks"
10
+ ASSOCIATED_DOMAINS = "com.apple.developer.associated-domains"
11
+ CODE_SIGN_ENTITLEMENTS = "CODE_SIGN_ENTITLEMENTS"
12
+ DEVELOPMENT_TEAM = "DEVELOPMENT_TEAM"
13
+ PRODUCT_BUNDLE_IDENTIFIER = "PRODUCT_BUNDLE_IDENTIFIER"
14
+ RELEASE_CONFIGURATION = "Release"
15
+
16
+ def add_keys_to_info_plist(project, target_name, keys, configuration = RELEASE_CONFIGURATION)
17
+ update_info_plist_setting project, target_name, configuration do |info_plist|
18
+ # add/overwrite Branch key(s)
19
+ if keys.count > 1
20
+ info_plist["branch_key"] = keys
21
+ elsif keys[:live]
22
+ info_plist["branch_key"] = keys[:live]
23
+ else # no need to validate here, which was done by the action
24
+ info_plist["branch_key"] = keys[:test]
25
+ end
26
+ end
27
+ end
28
+
29
+ def add_branch_universal_link_domains_to_info_plist(project, target_name, domains, configuration = RELEASE_CONFIGURATION)
30
+ # Add all supplied domains unless all are app.link domains.
31
+ return if domains.all? { |d| d =~ /app\.link$/ }
32
+
33
+ update_info_plist_setting project, target_name, configuration do |info_plist|
34
+ info_plist["branch_universal_link_domains"] = domains
35
+ end
36
+ end
37
+
38
+ def update_info_plist_setting(project, target_name, configuration = RELEASE_CONFIGURATION, &b)
39
+ # raises
40
+ target = target_from_project project, target_name
41
+
42
+ # find the Info.plist paths for this configuration
43
+ info_plist_path = expanded_build_setting target, "INFOPLIST_FILE", configuration
44
+
45
+ raise "Info.plist not found for configuration #{configuration}" if info_plist_path.nil?
46
+
47
+ project_parent = File.dirname project.path
48
+
49
+ info_plist_path = File.expand_path info_plist_path, project_parent
50
+
51
+ # try to open and parse the Info.plist (raises)
52
+ info_plist = File.open(info_plist_path) { |f| Plist.parse_xml f }
53
+ raise "Failed to parse #{info_plist_path}" if info_plist.nil?
54
+
55
+ yield info_plist
56
+
57
+ Plist::Emit.save_plist info_plist, info_plist_path
58
+ add_change info_plist_path
59
+ end
60
+
61
+ def add_universal_links_to_project(project, target_name, domains, remove_existing, configuration = RELEASE_CONFIGURATION)
62
+ # raises
63
+ target = target_from_project project, target_name
64
+
65
+ relative_entitlements_path = expanded_build_setting target, CODE_SIGN_ENTITLEMENTS, configuration
66
+ project_parent = File.dirname project.path
67
+
68
+ if relative_entitlements_path.nil?
69
+ relative_entitlements_path = File.join target.name, "#{target.name}.entitlements"
70
+ entitlements_path = File.expand_path relative_entitlements_path, project_parent
71
+
72
+ # Add CODE_SIGN_ENTITLEMENTS setting to each configuration
73
+ target.build_configuration_list.set_setting CODE_SIGN_ENTITLEMENTS, relative_entitlements_path
74
+
75
+ # Add the file to the project
76
+ project.new_file relative_entitlements_path
77
+
78
+ entitlements = {}
79
+ current_domains = []
80
+
81
+ add_change project.path
82
+ new_path = entitlements_path
83
+ else
84
+ entitlements_path = File.expand_path relative_entitlements_path, project_parent
85
+ # Raises
86
+ entitlements = File.open(entitlements_path) { |f| Plist.parse_xml f }
87
+ raise "Failed to parse entitlements file #{entitlements_path}" if entitlements.nil?
88
+
89
+ if remove_existing
90
+ current_domains = []
91
+ else
92
+ current_domains = entitlements[ASSOCIATED_DOMAINS]
93
+ end
94
+ end
95
+
96
+ current_domains += domains.map { |d| "#{APPLINKS}:#{d}" }
97
+ all_domains = current_domains.uniq
98
+
99
+ entitlements[ASSOCIATED_DOMAINS] = all_domains
100
+
101
+ Plist::Emit.save_plist entitlements, entitlements_path
102
+ add_change entitlements_path
103
+
104
+ new_path
105
+ end
106
+
107
+ def team_and_bundle_from_app_id(identifier)
108
+ team = identifier.sub(/\..+$/, "")
109
+ bundle = identifier.sub(/^[^.]+\./, "")
110
+ [team, bundle]
111
+ end
112
+
113
+ def update_team_and_bundle_ids_from_aasa_file(project, target_name, domain)
114
+ # raises
115
+ identifiers = app_ids_from_aasa_file domain
116
+ raise "Multiple appIDs found in AASA file" if identifiers.count > 1
117
+
118
+ identifier = identifiers[0]
119
+ team, bundle = team_and_bundle_from_app_id identifier
120
+
121
+ update_team_and_bundle_ids project, target_name, team, bundle
122
+ add_change project.path.expand_path
123
+ end
124
+
125
+ def validate_team_and_bundle_ids_from_aasa_files(project, target_name, domains = [], remove_existing = false, configuration = RELEASE_CONFIGURATION)
126
+ @errors = []
127
+ valid = true
128
+
129
+ # Include any domains already in the project.
130
+ # Raises. Returns a non-nil array of strings.
131
+ if remove_existing
132
+ # Don't validate domains to be removed (#16)
133
+ all_domains = domains
134
+ else
135
+ all_domains = (domains + domains_from_project(project, target_name, configuration)).uniq
136
+ end
137
+
138
+ if all_domains.empty?
139
+ # Cannot get here from SetupBranchAction, since the domains passed in will never be empty.
140
+ # If called from ValidateUniversalLinksAction, this is a failure, possibly caused by
141
+ # failure to add applinks:.
142
+ @errors << "No Universal Link domains in project. Be sure each Universal Link domain is prefixed with applinks:."
143
+ return false
144
+ end
145
+
146
+ all_domains.each do |domain|
147
+ domain_valid = validate_team_and_bundle_ids project, target_name, domain, configuration
148
+ valid &&= domain_valid
149
+ say "Valid Universal Link configuration for #{domain} ✅" if domain_valid
150
+ end
151
+ valid
152
+ end
153
+
154
+ def app_ids_from_aasa_file(domain)
155
+ data = contents_of_aasa_file domain
156
+ # errors reported in the method above
157
+ return nil if data.nil?
158
+
159
+ # raises
160
+ file = JSON.parse data
161
+
162
+ applinks = file[APPLINKS]
163
+ @errors << "[#{domain}] No #{APPLINKS} found in AASA file" and return if applinks.nil?
164
+
165
+ details = applinks["details"]
166
+ @errors << "[#{domain}] No details found for #{APPLINKS} in AASA file" and return if details.nil?
167
+
168
+ identifiers = details.map { |d| d["appID"] }.uniq
169
+ @errors << "[#{domain}] No appID found in AASA file" and return if identifiers.count <= 0
170
+ identifiers
171
+ rescue JSON::ParserError => e
172
+ @errors << "[#{domain}] Failed to parse AASA file: #{e.message}"
173
+ nil
174
+ end
175
+
176
+ def contents_of_aasa_file(domain)
177
+ uris = [
178
+ URI("https://#{domain}/.well-known/apple-app-site-association"),
179
+ URI("https://#{domain}/apple-app-site-association")
180
+ # URI("http://#{domain}/.well-known/apple-app-site-association"),
181
+ # URI("http://#{domain}/apple-app-site-association")
182
+ ]
183
+
184
+ data = nil
185
+
186
+ uris.each do |uri|
187
+ break unless data.nil?
188
+
189
+ Net::HTTP.start uri.host, uri.port, use_ssl: uri.scheme == "https" do |http|
190
+ request = Net::HTTP::Get.new uri
191
+ response = http.request request
192
+
193
+ # Better to use Net::HTTPRedirection and Net::HTTPSuccess here, but
194
+ # having difficulty with the unit tests.
195
+ if (300..399).cover?(response.code.to_i)
196
+ say "#{uri} cannot result in a redirect. Ignoring."
197
+ next
198
+ elsif response.code.to_i != 200
199
+ # Try the next URI.
200
+ say "Could not retrieve #{uri}: #{response.code} #{response.message}. Ignoring."
201
+ next
202
+ end
203
+
204
+ content_type = response["Content-type"]
205
+ @errors << "[#{domain}] AASA Response does not contain a Content-type header" and next if content_type.nil?
206
+
207
+ case content_type
208
+ when %r{application/pkcs7-mime}
209
+ # Verify/decrypt PKCS7 (non-Branch domains)
210
+ cert_store = OpenSSL::X509::Store.new
211
+ signature = OpenSSL::PKCS7.new response.body
212
+ # raises
213
+ signature.verify nil, cert_store, nil, OpenSSL::PKCS7::NOVERIFY
214
+ data = signature.data
215
+ else
216
+ @error << "[#{domain}] Unsigned AASA files must be served via HTTPS" and next if uri.scheme == "http"
217
+ data = response.body
218
+ end
219
+
220
+ say "GET #{uri}: #{response.code} #{response.message} (Content-type:#{content_type}) ✅"
221
+ end
222
+ end
223
+
224
+ @errors << "[#{domain}] Failed to retrieve AASA file" and return nil if data.nil?
225
+
226
+ data
227
+ rescue IOError, SocketError => e
228
+ @errors << "[#{domain}] Socket error: #{e.message}"
229
+ nil
230
+ rescue OpenSSL::PKCS7::PKCS7Error => e
231
+ @errors << "[#{domain}] Failed to verify signed AASA file: #{e.message}"
232
+ nil
233
+ end
234
+
235
+ def validate_team_and_bundle_ids(project, target_name, domain, configuration)
236
+ # raises
237
+ target = target_from_project project, target_name
238
+
239
+ product_bundle_identifier = expanded_build_setting target, PRODUCT_BUNDLE_IDENTIFIER, configuration
240
+ development_team = expanded_build_setting target, DEVELOPMENT_TEAM, configuration
241
+
242
+ identifiers = app_ids_from_aasa_file domain
243
+ return false if identifiers.nil?
244
+
245
+ app_id = "#{development_team}.#{product_bundle_identifier}"
246
+ match_found = identifiers.include? app_id
247
+
248
+ unless match_found
249
+ @errors << "[#{domain}] appID mismatch. Project: #{app_id}. AASA: #{identifiers}"
250
+ end
251
+
252
+ match_found
253
+ end
254
+
255
+ def validate_project_domains(expected, project, target, configuration = RELEASE_CONFIGURATION)
256
+ @errors = []
257
+ project_domains = domains_from_project project, target, configuration
258
+ valid = expected.count == project_domains.count
259
+ if valid
260
+ sorted = expected.sort
261
+ project_domains.sort.each_with_index do |domain, index|
262
+ valid = false and break unless sorted[index] == domain
263
+ end
264
+ end
265
+
266
+ unless valid
267
+ @errors << "Project domains do not match :domains parameter"
268
+ @errors << "Project domains: #{project_domains}"
269
+ @errors << ":domains parameter: #{expected}"
270
+ end
271
+
272
+ valid
273
+ end
274
+
275
+ def update_team_and_bundle_ids(project, target_name, team, bundle)
276
+ # raises
277
+ target = target_from_project project, target_name
278
+
279
+ target.build_configuration_list.set_setting PRODUCT_BUNDLE_IDENTIFIER, bundle
280
+ target.build_configuration_list.set_setting DEVELOPMENT_TEAM, team
281
+
282
+ # also update the team in the first test target
283
+ target = project.targets.find(&:test_target_type?)
284
+ return if target.nil?
285
+
286
+ target.build_configuration_list.set_setting DEVELOPMENT_TEAM, team
287
+ end
288
+
289
+ def target_from_project(project, target_name)
290
+ if target_name
291
+ target = project.targets.find { |t| t.name == target_name }
292
+ raise "Target #{target} not found" if target.nil?
293
+ else
294
+ # find the first application target
295
+ target = project.targets.find { |t| !t.extension_target_type? && !t.test_target_type? }
296
+ raise "No application target found" if target.nil?
297
+ end
298
+ target
299
+ end
300
+
301
+ def domains_from_project(project, target_name, configuration = RELEASE_CONFIGURATION)
302
+ # Raises. Does not return nil.
303
+ target = target_from_project project, target_name
304
+
305
+ relative_entitlements_path = expanded_build_setting target, CODE_SIGN_ENTITLEMENTS, configuration
306
+ return [] if relative_entitlements_path.nil?
307
+
308
+ project_parent = File.dirname project.path
309
+ entitlements_path = File.expand_path relative_entitlements_path, project_parent
310
+
311
+ # Raises
312
+ entitlements = File.open(entitlements_path) { |f| Plist.parse_xml f }
313
+ raise "Failed to parse entitlements file #{entitlements_path}" if entitlements.nil?
314
+
315
+ entitlements[ASSOCIATED_DOMAINS].select { |d| d =~ /^applinks:/ }.map { |d| d.sub(/^applinks:/, "") }
316
+ end
317
+
318
+ def expanded_build_setting(target, setting_name, configuration)
319
+ setting_value = target.resolved_build_setting(setting_name)[configuration]
320
+ return if setting_value.nil?
321
+
322
+ search_position = 0
323
+ while (matches = /\$\(([^(){}]*)\)|\$\{([^(){}]*)\}/.match(setting_value, search_position))
324
+ macro_name = matches[1] || matches[2]
325
+ search_position = setting_value.index(macro_name) - 2
326
+
327
+ expanded_macro = macro_name == "SRCROOT" ? "." : expanded_build_setting(target, macro_name, configuration)
328
+ search_position += macro_name.length + 3 and next if expanded_macro.nil?
329
+
330
+ setting_value.gsub!(/\$\(#{macro_name}\)|\$\{#{macro_name}\}/, expanded_macro)
331
+ search_position += expanded_macro.length
332
+ end
333
+ setting_value
334
+ end
335
+
336
+ def add_system_frameworks(project, target_name, frameworks)
337
+ target = target_from_project project, target_name
338
+
339
+ target.add_system_framework frameworks
340
+ end
341
+
342
+ def patch_app_delegate_swift(project)
343
+ app_delegate_swift = project.files.find { |f| f.path =~ /AppDelegate.swift$/ }
344
+ return false if app_delegate_swift.nil?
345
+
346
+ app_delegate_swift_path = app_delegate_swift.real_path.to_s
347
+
348
+ app_delegate = File.open(app_delegate_swift_path, &:read)
349
+ return false if app_delegate =~ /import\s+Branch/
350
+
351
+ say "Patching #{app_delegate_swift_path}"
352
+
353
+ apply_patch(
354
+ files: app_delegate_swift_path,
355
+ regexp: /^\s*import .*$/,
356
+ text: "\nimport Branch",
357
+ mode: :prepend
358
+ )
359
+
360
+ # TODO: This is Swift 3. Support other versions, esp. 4.
361
+ init_session_text = <<-EOF
362
+ #if DEBUG
363
+ Branch.setUseTestBranchKey(true)
364
+ #endif
365
+
366
+ Branch.getInstance().initSession(launchOptions: launchOptions) {
367
+ universalObject, linkProperties, error in
368
+
369
+ // TODO: Route Branch links
370
+ }
371
+ EOF
372
+
373
+ apply_patch(
374
+ files: app_delegate_swift_path,
375
+ regexp: /didFinishLaunchingWithOptions.*?\{[^\n]*\n/m,
376
+ text: init_session_text,
377
+ mode: :append
378
+ )
379
+
380
+ unless app_delegate =~ /application:.*continueUserActivity:.*restorationHandler:/
381
+ # Add the application:continueUserActivity:restorationHandler method if it does not exist
382
+ continue_user_activity_text = <<-EOF
383
+
384
+
385
+ func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
386
+ return Branch.getInstance().continue(userActivity)
387
+ }
388
+ EOF
389
+
390
+ apply_patch(
391
+ files: app_delegate_swift_path,
392
+ regexp: /\n\s*\}[^{}]*\Z/m,
393
+ text: continue_user_activity_text,
394
+ mode: :prepend
395
+ )
396
+ end
397
+
398
+ add_change app_delegate_swift_path
399
+ true
400
+ end
401
+
402
+ def patch_app_delegate_objc(project)
403
+ app_delegate_objc = project.files.find { |f| f.path =~ /AppDelegate.m$/ }
404
+ return false if app_delegate_objc.nil?
405
+
406
+ app_delegate_objc_path = app_delegate_objc.real_path.to_s
407
+
408
+ app_delegate = File.open(app_delegate_objc_path, &:read)
409
+ return false if app_delegate =~ %r{^\s+#import\s+<Branch/Branch.h>|^\s+@import\s+Branch;}
410
+
411
+ say "Patching #{app_delegate_objc_path}"
412
+
413
+ apply_patch(
414
+ files: app_delegate_objc_path,
415
+ regexp: /^\s+@import|^\s+#import.*$/,
416
+ text: "\n#import <Branch/Branch.h>",
417
+ mode: :prepend
418
+ )
419
+
420
+ init_session_text = <<-EOF
421
+ #ifdef DEBUG
422
+ [Branch setUseTestBranchKey:YES];
423
+ #endif // DEBUG
424
+
425
+ [[Branch getInstance] initSessionWithLaunchOptions:launchOptions
426
+ andRegisterDeepLinkHandlerUsingBranchUniversalObject:^(BranchUniversalObject *universalObject, BranchLinkProperties *linkProperties, NSError *error){
427
+ // TODO: Route Branch links
428
+ }];
429
+ EOF
430
+
431
+ apply_patch(
432
+ files: app_delegate_objc_path,
433
+ regexp: /didFinishLaunchingWithOptions.*?\{[^\n]*\n/m,
434
+ text: init_session_text,
435
+ mode: :append
436
+ )
437
+
438
+ unless app_delegate =~ /application:.*continueUserActivity:.*restorationHandler:/
439
+ # Add the application:continueUserActivity:restorationHandler method if it does not exist
440
+ continue_user_activity_text = <<-EOF
441
+
442
+
443
+ - (BOOL)application:(UIApplication *)app continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler
444
+ {
445
+ return [[Branch getInstance] continueUserActivity:userActivity];
446
+ }
447
+ EOF
448
+
449
+ apply_patch(
450
+ files: app_delegate_objc_path,
451
+ regexp: /\n\s*@end[^@]*\Z/m,
452
+ text: continue_user_activity_text,
453
+ mode: :prepend
454
+ )
455
+ end
456
+
457
+ add_change app_delegate_objc_path
458
+ true
459
+ end
460
+
461
+ def patch_podfile(podfile_path)
462
+ podfile = File.open(podfile_path, &:read)
463
+
464
+ # Podfile already contains the Branch pod
465
+ return false if podfile =~ /pod\s+('Branch'|"Branch")/
466
+
467
+ say "Adding pod \"Branch\" to #{podfile_path}"
468
+
469
+ # TODO: Improve this patch. Should work in the majority of cases for now.
470
+ apply_patch(
471
+ files: podfile_path,
472
+ regexp: /^(\s*)pod\s*/,
473
+ text: "\n\\1pod \"Branch\"\n",
474
+ mode: :prepend
475
+ )
476
+
477
+ true
478
+ end
479
+
480
+ def patch_cartfile(cartfile_path)
481
+ cartfile = File.open(cartfile_path, &:read)
482
+
483
+ # Cartfile already contains the Branch framework
484
+ return false if cartfile =~ /git.+Branch/
485
+
486
+ say "Adding \"Branch\" to #{cartfile_path}"
487
+
488
+ apply_patch(
489
+ files: cartfile_path,
490
+ regexp: /\z/,
491
+ text: "git \"https://github.com/BranchMetrics/ios-branch-deep-linking\"\n",
492
+ mode: :append
493
+ )
494
+
495
+ true
496
+ end
497
+ end
498
+ end
499
+ end
@@ -0,0 +1,3 @@
1
+ module BranchIOCLI
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,213 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: branch_io_cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Branch
8
+ - Jimmy Dee
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2017-10-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: commander
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: pattern_patch
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: plist
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: xcodeproj
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: pry
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: bundler
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rspec
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: rake
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rspec-simplecov
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rubocop
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: simplecov
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ description: Set up mobile app projects (currently iOS only) to use the Branch SDK
169
+ without opening Xcode. Validate the Universal Link settings for any project.
170
+ email:
171
+ - integrations@branch.io
172
+ - jgvdthree@gmail.com
173
+ executables:
174
+ - branch_io
175
+ extensions: []
176
+ extra_rdoc_files: []
177
+ files:
178
+ - LICENSE
179
+ - README.md
180
+ - bin/branch_io
181
+ - lib/branch_io_cli.rb
182
+ - lib/branch_io_cli/cli.rb
183
+ - lib/branch_io_cli/command.rb
184
+ - lib/branch_io_cli/helper.rb
185
+ - lib/branch_io_cli/helper/android_helper.rb
186
+ - lib/branch_io_cli/helper/branch_helper.rb
187
+ - lib/branch_io_cli/helper/ios_helper.rb
188
+ - lib/branch_io_cli/version.rb
189
+ homepage: http://github.com/BranchMetrics/branch_io_cli
190
+ licenses:
191
+ - MIT
192
+ metadata: {}
193
+ post_install_message:
194
+ rdoc_options: []
195
+ require_paths:
196
+ - lib
197
+ required_ruby_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ required_rubygems_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ requirements: []
208
+ rubyforge_project:
209
+ rubygems_version: 2.6.14
210
+ signing_key:
211
+ specification_version: 4
212
+ summary: Branch.io command-line interface for mobile app integration
213
+ test_files: []