branch_io_cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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.
|
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: []
|