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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/bin/branch_io +5 -0
- data/lib/branch_io_cli.rb +4 -0
- data/lib/branch_io_cli/cli.rb +59 -0
- data/lib/branch_io_cli/command.rb +281 -0
- data/lib/branch_io_cli/helper.rb +1 -0
- data/lib/branch_io_cli/helper/android_helper.rb +86 -0
- data/lib/branch_io_cli/helper/branch_helper.rb +39 -0
- data/lib/branch_io_cli/helper/ios_helper.rb +499 -0
- data/lib/branch_io_cli/version.rb +3 -0
- metadata +213 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
[](https://rubygems.org/gems/branch_io_cli)
|
6
|
+
[](https://rubygems.org/gems/branch_io_cli)
|
7
|
+
[](https://github.com/BranchMetrics/branch_io_cli/blob/master/LICENSE)
|
8
|
+
[](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.
|
data/bin/branch_io
ADDED
@@ -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
|
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: []
|