branch_io_cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 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: []