motion-sparkle-sandbox 2.1.0 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/bundle +23 -14
- data/lib/motion/project/appcast.rb +237 -127
- data/lib/motion/project/project.rb +1 -0
- data/lib/motion/project/rake_tasks.rb +15 -1
- data/lib/motion/project/setup.rb +2 -2
- data/lib/motion/project/sparkle.rb +107 -34
- data/lib/motion-sparkle-sandbox/version.rb +1 -1
- data/sample-app/.gitignore +1 -0
- data/sample-app/Rakefile +5 -2
- data/spec/appcast_spec.rb +60 -0
- data/spec/sparkle_spec.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca50d516f00ac1b85b4212983fbe11e53f1d859ebbb18b2d3e4fad974a055523
|
4
|
+
data.tar.gz: 263ad26c09854be232dd7a19485270b02d33b148838925f09f06c874eef2f993
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85a7857cc96ef6e0a54bca54bdb416316781101cd7a85510c4905d8ecca79c3558795c89f83225df83db2f914052be24a570ddf03d093f80ea0121b75324141a
|
7
|
+
data.tar.gz: fecced623e72e228bc0f64025c40aae21bd89819de2a7c8d655e8f04043be0af6d68820372a5018dbad69302d67678b1feccacd6efe683b84d583ff23f899309
|
data/bin/bundle
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
require "rubygems"
|
12
12
|
|
13
13
|
m = Module.new do
|
14
|
-
|
14
|
+
module_function
|
15
15
|
|
16
16
|
def invoked_as_script?
|
17
17
|
File.expand_path($0) == File.expand_path(__FILE__)
|
@@ -31,7 +31,7 @@ m = Module.new do
|
|
31
31
|
bundler_version = a
|
32
32
|
end
|
33
33
|
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
|
34
|
-
bundler_version = $1
|
34
|
+
bundler_version = $1
|
35
35
|
update_index = i
|
36
36
|
end
|
37
37
|
bundler_version
|
@@ -61,32 +61,41 @@ m = Module.new do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def bundler_version
|
64
|
-
@bundler_version ||=
|
64
|
+
@bundler_version ||=
|
65
65
|
env_var_version || cli_arg_version ||
|
66
|
-
lockfile_version
|
67
|
-
|
66
|
+
lockfile_version
|
67
|
+
end
|
68
|
+
|
69
|
+
def bundler_requirement
|
70
|
+
return "#{Gem::Requirement.default}.a" unless bundler_version
|
71
|
+
|
72
|
+
bundler_gem_version = Gem::Version.new(bundler_version)
|
73
|
+
|
74
|
+
requirement = bundler_gem_version.approximate_recommendation
|
75
|
+
|
76
|
+
return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
|
77
|
+
|
78
|
+
requirement += ".a" if bundler_gem_version.prerelease?
|
79
|
+
|
80
|
+
requirement
|
68
81
|
end
|
69
82
|
|
70
83
|
def load_bundler!
|
71
84
|
ENV["BUNDLE_GEMFILE"] ||= gemfile
|
72
85
|
|
73
|
-
|
74
|
-
activate_bundler(bundler_version.dup)
|
86
|
+
activate_bundler
|
75
87
|
end
|
76
88
|
|
77
|
-
def activate_bundler
|
78
|
-
if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
|
79
|
-
bundler_version = "< 2"
|
80
|
-
end
|
89
|
+
def activate_bundler
|
81
90
|
gem_error = activation_error_handling do
|
82
|
-
gem "bundler",
|
91
|
+
gem "bundler", bundler_requirement
|
83
92
|
end
|
84
93
|
return if gem_error.nil?
|
85
94
|
require_error = activation_error_handling do
|
86
95
|
require "bundler/version"
|
87
96
|
end
|
88
|
-
return if require_error.nil? && Gem::Requirement.new(
|
89
|
-
warn "Activating bundler (#{
|
97
|
+
return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
|
98
|
+
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
|
90
99
|
exit 42
|
91
100
|
end
|
92
101
|
|
@@ -1,45 +1,179 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
3
4
|
module Motion
|
4
5
|
module Project
|
5
6
|
class Sparkle
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
class Appcast
|
8
|
+
PARAMS = %i[
|
9
|
+
package_base_url
|
10
|
+
package_filename
|
11
|
+
notes_base_url
|
12
|
+
notes_filename
|
13
|
+
use_exported_private_key
|
14
|
+
base_url
|
15
|
+
releases_folder
|
16
|
+
feed_base_url
|
17
|
+
feed_filename
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
CLI_OPTIONS = {
|
21
|
+
account: '--account',
|
22
|
+
private_eddsa_key: '-s',
|
23
|
+
download_url_prefix: '--download-url-prefix',
|
24
|
+
release_notes_url_prefix: '--release-notes-url-prefix',
|
25
|
+
full_release_notes_url: '--full-release-notes-url',
|
26
|
+
link: '--link',
|
27
|
+
versions: '--versions',
|
28
|
+
maximum_deltas: '--maximum-deltas',
|
29
|
+
delta_compression: '--delta-compression',
|
30
|
+
delta_compression_level: '--delta-compression-level',
|
31
|
+
channel: '--channel',
|
32
|
+
major_version: '--major-version',
|
33
|
+
ignore_skipped_upgrades_below_version: '--ignore-skipped-upgrades-below-version',
|
34
|
+
phased_rollout_interval: '--phased-rollout-interval',
|
35
|
+
critical_update_version: '--critical-update-version',
|
36
|
+
informational_update_versions: '--informational-update-versions',
|
37
|
+
output_path: '-o'
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
attr_accessor :base_url,
|
41
|
+
:feed_base_url,
|
42
|
+
:feed_filename,
|
43
|
+
:notes_filename,
|
44
|
+
:package_filename,
|
45
|
+
:releases_folder,
|
46
|
+
:use_exported_private_key,
|
47
|
+
:cli_options
|
48
|
+
attr_writer :notes_base_url,
|
49
|
+
:package_base_url
|
50
|
+
|
51
|
+
def initialize(sparkle_object)
|
52
|
+
@sparkle = sparkle_object
|
53
|
+
@cli_options = {
|
54
|
+
account: 'ed25519' # Sparkle's default account
|
55
|
+
}
|
56
|
+
|
57
|
+
@feed_base_url = nil
|
58
|
+
@feed_filename = 'releases.xml'
|
59
|
+
@notes_base_url = nil
|
60
|
+
@notes_filename = nil
|
61
|
+
@package_base_url = nil
|
62
|
+
@package_filename = nil
|
63
|
+
@base_url = nil
|
64
|
+
@releases_folder = nil
|
65
|
+
@use_exported_private_key = false
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_option(key, value)
|
69
|
+
if CLI_OPTIONS.keys.include?(key)
|
70
|
+
cli_options[key] = value
|
71
|
+
elsif PARAMS.include?(key)
|
72
|
+
send("#{key}=", value)
|
73
|
+
else
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def feed_url
|
81
|
+
"#{feed_base_url || base_url}#{feed_filename}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def notes_base_url
|
85
|
+
@notes_base_url || base_url
|
86
|
+
end
|
14
87
|
|
15
|
-
|
88
|
+
def package_base_url
|
89
|
+
@package_base_url || base_url
|
90
|
+
end
|
91
|
+
|
92
|
+
def prepare_args
|
93
|
+
args = []
|
94
|
+
|
95
|
+
account(args)
|
96
|
+
private_eddsa_key(args)
|
97
|
+
download_url_prefix(args)
|
98
|
+
release_notes_url_prefix(args)
|
99
|
+
full_release_notes_url(args)
|
100
|
+
link(args)
|
101
|
+
versions(args)
|
102
|
+
maximum_deltas(args)
|
103
|
+
delta_compression(args)
|
104
|
+
delta_compression_level(args)
|
105
|
+
channel(args)
|
106
|
+
major_version(args)
|
107
|
+
ignore_skipped_upgrades_below_version(args)
|
108
|
+
phased_rollout_interval(args)
|
109
|
+
critical_update_version(args)
|
110
|
+
informational_update_versions(args)
|
111
|
+
output_path(args)
|
112
|
+
|
113
|
+
args
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
16
117
|
|
17
|
-
|
118
|
+
# --account <account> The account name in your keychain associated with
|
119
|
+
# your private EdDSA (ed25519) key to use for signing
|
120
|
+
# new updates. (default: ed25519)
|
121
|
+
def account(args)
|
122
|
+
return unless cli_options[:account].present?
|
18
123
|
|
19
|
-
|
20
|
-
|
124
|
+
args << "--account=#{cli_options[:account]}"
|
125
|
+
end
|
21
126
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
127
|
+
# -s <private-EdDSA-key> The private EdDSA string (128 characters). If not
|
128
|
+
# specified, the private EdDSA key will be read from
|
129
|
+
# the Keychain instead.
|
130
|
+
def private_eddsa_key(args)
|
131
|
+
if cli_options[:private_eddsa_key].present?
|
132
|
+
args << "-s=#{cli_options[:private_eddsa_key]}"
|
133
|
+
elsif use_exported_private_key && File.exist?(private_key_path)
|
134
|
+
private_key = File.read(private_key_path)
|
135
|
+
args << "-s=#{private_key}"
|
136
|
+
end
|
28
137
|
end
|
29
138
|
|
30
139
|
# --download-url-prefix <url> A URL that will be used as prefix for the URL from
|
31
140
|
# where updates will be downloaded.
|
32
|
-
args
|
141
|
+
def download_url_prefix(args)
|
142
|
+
if cli_options[:download_url_prefix].present?
|
143
|
+
args << "--download-url-prefix=#{cli_options[:download_url_prefix]}"
|
144
|
+
elsif package_base_url.present?
|
145
|
+
args << "--download-url-prefix=#{package_base_url}"
|
146
|
+
end
|
147
|
+
end
|
33
148
|
|
34
149
|
# --release-notes-url-prefix <url> A URL that will be used as prefix for constructing
|
35
150
|
# URLs for release notes.
|
36
|
-
args
|
151
|
+
def release_notes_url_prefix(args)
|
152
|
+
if cli_options[:release_notes_url_prefix].present?
|
153
|
+
args << "--release-notes-url-prefix=#{cli_options[:release_notes_url_prefix]}"
|
154
|
+
elsif notes_base_url.present?
|
155
|
+
args << "--release-notes-url-prefix=#{notes_base_url}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# --full-release-notes-url <url>
|
160
|
+
# A URL that will be used for the full release notes.
|
161
|
+
def full_release_notes_url(args)
|
162
|
+
return unless cli_options[:full_release_notes_url].present?
|
163
|
+
|
164
|
+
args << "--full-release-notes-url=#{cli_options[:full_release_notes_url]}"
|
165
|
+
end
|
37
166
|
|
38
167
|
# --link <link> A URL to the application's website which Sparkle may
|
39
168
|
# use for directing users to if they cannot download a
|
40
169
|
# new update from within the application. This will be
|
41
170
|
# used for new generated update items. By default, no
|
42
171
|
# product link is used.
|
172
|
+
def link(args)
|
173
|
+
return unless cli_options[:link].present?
|
174
|
+
|
175
|
+
args << "--link=#{cli_options[:link]}"
|
176
|
+
end
|
43
177
|
|
44
178
|
# --versions <versions> An optional comma delimited list of application
|
45
179
|
# versions (specified by CFBundleVersion) to generate
|
@@ -47,27 +181,88 @@ module Motion
|
|
47
181
|
# are inferred from the available archives and are only
|
48
182
|
# generated if they are in the latest 5 updates in the
|
49
183
|
# appcast.
|
184
|
+
def versions(args)
|
185
|
+
return unless cli_options[:versions].present?
|
186
|
+
|
187
|
+
args << "--versions=#{cli_options[:versions]}"
|
188
|
+
end
|
50
189
|
|
51
190
|
# --maximum-deltas <maximum-deltas>
|
52
191
|
# The maximum number of delta items to create for the
|
53
192
|
# latest update for each minimum required operating
|
54
193
|
# system. (default: 5)
|
194
|
+
def maximum_deltas(args)
|
195
|
+
return unless cli_options[:maximum_deltas].present?
|
196
|
+
|
197
|
+
args << "--maximum-deltas=#{cli_options[:maximum_deltas]}"
|
198
|
+
end
|
199
|
+
|
200
|
+
# --delta-compression <delta-compression>
|
201
|
+
# The compression method to use for generating delta
|
202
|
+
# updates. Supported methods for version 3 delta files
|
203
|
+
# are 'lzma', 'bzip2', 'zlib', 'lzfse', 'lz4', 'none',
|
204
|
+
# and 'default'. Note that version 2 delta files only
|
205
|
+
# support 'bzip2', 'none', and 'default' so other
|
206
|
+
# methods will be ignored if version 2 files are being
|
207
|
+
# generated. The 'default' compression for version 3
|
208
|
+
# delta files is currently lzma. (default: default)
|
209
|
+
def delta_compression(args)
|
210
|
+
return unless cli_options[:delta_compression].present?
|
211
|
+
|
212
|
+
args << "--delta-compression=#{cli_options[:delta_compression]}"
|
213
|
+
end
|
214
|
+
|
215
|
+
# --delta-compression-level <delta-compression-level>
|
216
|
+
# The compression level to use for generating delta
|
217
|
+
# updates. This only applies if the compression method
|
218
|
+
# used is bzip2 which accepts values from 1 - 9. A
|
219
|
+
# special value of 0 will use the default compression
|
220
|
+
# level. (default: 0)
|
221
|
+
def delta_compression_level(args)
|
222
|
+
return unless cli_options[:delta_compression_level].present?
|
223
|
+
|
224
|
+
args << "--delta-compression-level=#{cli_options[:delta_compression_level]}"
|
225
|
+
end
|
55
226
|
|
56
227
|
# --channel <channel-name>
|
57
228
|
# The Sparkle channel name that will be used for
|
58
229
|
# generating new updates. By default, no channel is
|
59
230
|
# used. Old applications need to be using Sparkle 2 to
|
60
231
|
# use this feature.
|
232
|
+
def channel(args)
|
233
|
+
return unless cli_options[:channel].present?
|
234
|
+
|
235
|
+
args << "--channel=#{cli_options[:channel]}"
|
236
|
+
end
|
61
237
|
|
62
238
|
# --major-version <major-version>
|
63
239
|
# The last major or minimum autoupdate sparkle:version
|
64
240
|
# that will be used for generating new updates. By
|
65
241
|
# default, no last major version is used.
|
242
|
+
def major_version(args)
|
243
|
+
return unless cli_options[:major_version].present?
|
244
|
+
|
245
|
+
args << "--major-version=#{cli_options[:major_version]}"
|
246
|
+
end
|
247
|
+
|
248
|
+
# --ignore-skipped-upgrades-below-version <below-version>
|
249
|
+
# Ignore skipped major upgrades below this specified
|
250
|
+
# version. Only applicable for major upgrades.
|
251
|
+
def ignore_skipped_upgrades_below_version(args)
|
252
|
+
return unless cli_options[:ignore_skipped_upgrades_below_version].present?
|
253
|
+
|
254
|
+
args << "--ignore-skipped-upgrades-below-version=#{cli_options[:ignore_skipped_upgrades_below_version]}"
|
255
|
+
end
|
66
256
|
|
67
257
|
# --phased-rollout-interval <phased-rollout-interval>
|
68
258
|
# The phased rollout interval in seconds that will be
|
69
259
|
# used for generating new updates. By default, no
|
70
260
|
# phased rollout interval is used.
|
261
|
+
def phased_rollout_interval(args)
|
262
|
+
return unless cli_options[:phased_rollout_interval].present?
|
263
|
+
|
264
|
+
args << "--phased-rollout-interval=#{cli_options[:phased_rollout_interval]}"
|
265
|
+
end
|
71
266
|
|
72
267
|
# --critical-update-version <critical-update-version>
|
73
268
|
# The last critical update sparkle:version that will be
|
@@ -76,125 +271,40 @@ module Motion
|
|
76
271
|
# from any application version. By default, no last
|
77
272
|
# critical update version is used. Old applications
|
78
273
|
# need to be using Sparkle 2 to use this feature.
|
274
|
+
def critical_update_version(args)
|
275
|
+
return unless cli_options[:critical_update_version].present?
|
276
|
+
|
277
|
+
args << "--critical-update-version=#{cli_options[:critical_update_version]}"
|
278
|
+
end
|
79
279
|
|
80
280
|
# --informational-update-versions <informational-update-versions>
|
81
281
|
# A comma delimited list of application
|
82
282
|
# sparkle:version's that will see newly generated
|
83
283
|
# updates as being informational only. An empty string
|
84
284
|
# argument will treat this update as informational
|
85
|
-
# coming from any application version.
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
285
|
+
# coming from any application version. Prefix a version
|
286
|
+
# string with '<' to indicate (eg "<2.5") to indicate
|
287
|
+
# older versions than the one specified should treat
|
288
|
+
# the update as informational only. By default, updates
|
289
|
+
# are not informational only. --link must also be
|
290
|
+
# provided. Old applications need to be using Sparkle 2
|
291
|
+
# to use this feature, and 2.1 or later to use the '<'
|
292
|
+
# upper bound feature.
|
293
|
+
def informational_update_versions(args)
|
294
|
+
return unless cli_options[:informational_update_versions].present?
|
295
|
+
|
296
|
+
args << "--informational-update-versions=#{cli_options[:informational_update_versions]}"
|
297
|
+
end
|
89
298
|
|
90
299
|
# -o <output-path> Path to filename for the generated appcast (allowed
|
91
300
|
# when only one will be created).
|
301
|
+
def output_path(args)
|
302
|
+
return unless cli_options[:output_path].present?
|
92
303
|
|
93
|
-
|
94
|
-
# option for transitioning to EdDSA from older updates.
|
95
|
-
# Note: only for supporting a legacy app that used DSA keys. Check if the
|
96
|
-
# default DSA key exists in `sparkle/config/dsa_priv.pem` and if it does,
|
97
|
-
# add it to the command.
|
98
|
-
if File.exist?(legacy_private_key_path)
|
99
|
-
App.info 'Sparkle', "Also signing with legacy DSA key at #{legacy_private_key_path}"
|
100
|
-
args << "-f=#{legacy_private_key_path}"
|
101
|
-
end
|
102
|
-
|
103
|
-
args << "-o=#{appcast_filename}" if appcast_filename.present?
|
104
|
-
|
105
|
-
App.info 'Executing', [generate_appcast_app, *args, path.to_s].join(' ')
|
106
|
-
|
107
|
-
results, status = Open3.capture2e(generate_appcast_app, *args, path.to_s)
|
108
|
-
|
109
|
-
App.info('Sparkle', "Saved appcast to `#{appcast_filename}`") if status.success?
|
110
|
-
puts results.indent(11)
|
111
|
-
|
112
|
-
return unless status.success?
|
113
|
-
|
114
|
-
puts
|
115
|
-
puts "SUFeedURL : #{feed_url}".indent(11)
|
116
|
-
puts "SUPublicEDKey : #{public_EdDSA_key}".indent(11)
|
117
|
-
end
|
118
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
119
|
-
|
120
|
-
def generate_appcast_help
|
121
|
-
generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast"
|
122
|
-
results, _status = Open3.capture2e(generate_appcast_app, '--help')
|
123
|
-
puts results
|
124
|
-
end
|
125
|
-
|
126
|
-
def create_release_notes
|
127
|
-
App.fail "Release notes template not found as expected at ./#{release_notes_template_path}" unless File.exist?(release_notes_template_path)
|
128
|
-
|
129
|
-
create_release_folder
|
130
|
-
|
131
|
-
File.open(release_notes_path.to_s, 'w') do |f|
|
132
|
-
template = File.read(release_notes_template_path)
|
133
|
-
f << ERB.new(template).result(binding)
|
134
|
-
end
|
135
|
-
|
136
|
-
App.info 'Create', "./#{release_notes_path}"
|
137
|
-
end
|
138
|
-
|
139
|
-
def release_notes_template_path
|
140
|
-
sparkle_config_path.join('release_notes.template.erb')
|
141
|
-
end
|
142
|
-
|
143
|
-
def release_notes_content_path
|
144
|
-
sparkle_config_path.join('release_notes.content.html')
|
145
|
-
end
|
146
|
-
|
147
|
-
def release_notes_path
|
148
|
-
sparkle_release_path + (appcast.notes_filename || "#{app_name}.#{@config.short_version}.html")
|
149
|
-
end
|
150
|
-
|
151
|
-
def release_notes_content
|
152
|
-
if File.exist?(release_notes_content_path)
|
153
|
-
File.read(release_notes_content_path)
|
154
|
-
else
|
155
|
-
App.fail "Missing #{release_notes_content_path}"
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def release_notes_html
|
160
|
-
release_notes_content
|
161
|
-
end
|
162
|
-
|
163
|
-
class Appcast
|
164
|
-
attr_accessor :base_url,
|
165
|
-
:feed_base_url,
|
166
|
-
:feed_filename,
|
167
|
-
:notes_filename,
|
168
|
-
:package_filename,
|
169
|
-
:archive_folder,
|
170
|
-
:use_exported_private_key
|
171
|
-
attr_writer :notes_base_url,
|
172
|
-
:package_base_url
|
173
|
-
|
174
|
-
def initialize
|
175
|
-
@feed_base_url = nil
|
176
|
-
@feed_filename = 'releases.xml'
|
177
|
-
@notes_base_url = nil
|
178
|
-
@notes_filename = nil
|
179
|
-
@package_base_url = nil
|
180
|
-
@package_filename = nil
|
181
|
-
@base_url = nil
|
182
|
-
@archive_folder = nil
|
183
|
-
@use_exported_private_key = false
|
184
|
-
end
|
185
|
-
|
186
|
-
def feed_url
|
187
|
-
"#{feed_base_url || base_url}#{feed_filename}"
|
188
|
-
end
|
189
|
-
|
190
|
-
def notes_base_url
|
191
|
-
@notes_base_url || base_url
|
192
|
-
end
|
193
|
-
|
194
|
-
def package_base_url
|
195
|
-
@package_base_url || base_url
|
304
|
+
args << "-o=#{cli_options[:output_path]}"
|
196
305
|
end
|
197
306
|
end
|
198
307
|
end
|
199
308
|
end
|
200
309
|
end
|
310
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -1,17 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Sparkle specific rake tasks
|
4
|
+
# rubocop:disable Metrics/BlockLength
|
4
5
|
namespace :sparkle do
|
5
6
|
desc 'Sparkle Help'
|
6
7
|
task :help do
|
7
8
|
puts <<~HELP
|
9
|
+
|
10
|
+
|
8
11
|
During initial Sparkle setup, run these rake tasks:
|
9
12
|
|
10
13
|
1. `rake sparkle:setup_certificates`
|
11
14
|
2. `rake sparkle:setup`
|
12
15
|
|
13
16
|
Then after running `rake build:release`, you can run
|
14
|
-
|
17
|
+
|
18
|
+
3. `rake sparkle:package` create the zipped package and release notes
|
19
|
+
4. `rake sparkle:copy_to_release` copy package/notes into release folder
|
20
|
+
5. `rake sparkle:generate_appcast` generate the appcast
|
15
21
|
HELP
|
16
22
|
end
|
17
23
|
|
@@ -40,6 +46,13 @@ namespace :sparkle do
|
|
40
46
|
sparkle.sign_package
|
41
47
|
end
|
42
48
|
|
49
|
+
desc 'Copy the release notes and zip archive to the release folder'
|
50
|
+
task :copy_to_release do
|
51
|
+
App.config_without_setup.build_mode = :release
|
52
|
+
sparkle = App.config.sparkle
|
53
|
+
sparkle.copy_to_release
|
54
|
+
end
|
55
|
+
|
43
56
|
desc "Generate the appcast xml feed using Sparkle's `generate_appcast`"
|
44
57
|
task :generate_appcast do
|
45
58
|
App.config_without_setup.build_mode = :release
|
@@ -83,3 +96,4 @@ namespace :clean do
|
|
83
96
|
end
|
84
97
|
end
|
85
98
|
end
|
99
|
+
# rubocop:enable Metrics/BlockLength
|
data/lib/motion/project/setup.rb
CHANGED
@@ -27,7 +27,7 @@ module Motion
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def check_public_key
|
30
|
-
return true if
|
30
|
+
return true if public_ed_dsa_key.present?
|
31
31
|
|
32
32
|
App.fail 'Sparkle :public_key is nil or blank. Please check your Rakefile.'
|
33
33
|
end
|
@@ -45,7 +45,7 @@ module Motion
|
|
45
45
|
App.fail "Missing `#{private_key_path}`. Please run `rake sparkle:setup_certificates` or check the docs to know where to put them."
|
46
46
|
end
|
47
47
|
|
48
|
-
unless
|
48
|
+
unless public_ed_dsa_key.present?
|
49
49
|
return false if silence
|
50
50
|
|
51
51
|
App.fail "Missing `#{public_key_path}`. Did you configure `release :public_key` correctly in the Rakefile? Advanced: recreate your public key with `rake sparkle:recreate_public_key`"
|
@@ -15,29 +15,22 @@ module Motion
|
|
15
15
|
# verify_installation
|
16
16
|
end
|
17
17
|
|
18
|
+
def after_initialize
|
19
|
+
self.feed_url = appcast.feed_url
|
20
|
+
end
|
21
|
+
|
18
22
|
def appcast
|
19
|
-
@appcast ||= Appcast.new
|
23
|
+
@appcast ||= Appcast.new(self)
|
20
24
|
end
|
21
25
|
|
22
26
|
def publish(key, value)
|
27
|
+
return if appcast.process_option(key, value)
|
28
|
+
|
23
29
|
case key
|
24
30
|
when :public_key
|
25
|
-
self.
|
26
|
-
when :base_url
|
27
|
-
appcast.base_url = value
|
28
|
-
self.feed_url = appcast.feed_url
|
29
|
-
when :feed_base_url
|
30
|
-
appcast.feed_base_url = value
|
31
|
-
self.feed_url = appcast.feed_url
|
32
|
-
when :feed_filename
|
33
|
-
appcast.feed_filename = value
|
34
|
-
self.feed_url = appcast.feed_url
|
31
|
+
self.public_ed_dsa_key = value
|
35
32
|
when :version
|
36
|
-
version
|
37
|
-
when :package_base_url, :package_filename, :notes_base_url, :notes_filename, :use_exported_private_key
|
38
|
-
appcast.send "#{key}=", value
|
39
|
-
when :archive_folder
|
40
|
-
appcast.archive_folder = value
|
33
|
+
version(value)
|
41
34
|
else
|
42
35
|
raise "Unknown Sparkle config option #{key}"
|
43
36
|
end
|
@@ -61,15 +54,13 @@ module Motion
|
|
61
54
|
@config.info_plist['SUFeedURL'] = url
|
62
55
|
end
|
63
56
|
|
64
|
-
|
65
|
-
def public_EdDSA_key
|
57
|
+
def public_ed_dsa_key
|
66
58
|
@config.info_plist['SUPublicEDKey']
|
67
59
|
end
|
68
60
|
|
69
|
-
def
|
61
|
+
def public_ed_dsa_key=(key)
|
70
62
|
@config.info_plist['SUPublicEDKey'] = key
|
71
63
|
end
|
72
|
-
# rubocop:enable Naming/MethodName
|
73
64
|
|
74
65
|
# File manipulation and certificates
|
75
66
|
|
@@ -109,7 +100,7 @@ module Motion
|
|
109
100
|
|
110
101
|
if appcast.use_exported_private_key && File.exist?(private_key_path)
|
111
102
|
App.info 'Sparkle', "Private key already exported at `#{private_key_path}` and will be used."
|
112
|
-
if
|
103
|
+
if public_ed_dsa_key.present?
|
113
104
|
App.info '', <<~EXISTS
|
114
105
|
SUPublicEDKey already set
|
115
106
|
|
@@ -139,12 +130,12 @@ module Motion
|
|
139
130
|
return
|
140
131
|
end
|
141
132
|
|
142
|
-
results, status = Open3.capture2e(generate_keys_app, '-p')
|
133
|
+
results, status = Open3.capture2e(generate_keys_app, '-p', '--account', appcast.cli_options[:account])
|
143
134
|
|
144
135
|
if status.success?
|
145
|
-
App.info 'Sparkle',
|
136
|
+
App.info 'Sparkle', "Public/private keys found in the keychain for account #{appcast.cli_options[:account]}"
|
146
137
|
|
147
|
-
if results.strip ==
|
138
|
+
if results.strip == public_ed_dsa_key
|
148
139
|
App.info 'Sparkle', 'Keychain public key matches `SUPublicEDKey`'
|
149
140
|
|
150
141
|
if appcast.use_exported_private_key && !File.exist?(private_key_path)
|
@@ -155,7 +146,7 @@ module Motion
|
|
155
146
|
Keychain public key DOES NOT match `SUPublicEDKey`
|
156
147
|
|
157
148
|
Keychain public key: #{results.strip}
|
158
|
-
SUPublicEDKey public key: #{
|
149
|
+
SUPublicEDKey public key: #{public_ed_dsa_key}
|
159
150
|
|
160
151
|
NOT_MATCHED
|
161
152
|
.indent(11, skip_first_line: true)
|
@@ -173,7 +164,7 @@ module Motion
|
|
173
164
|
def create_private_key
|
174
165
|
App.info 'Sparkle',
|
175
166
|
'Generating a new signing key into the Keychain. This may take a moment, depending on your machine.'
|
176
|
-
results, status = Open3.capture2e(generate_keys_app)
|
167
|
+
results, status = Open3.capture2e(generate_keys_app, '--account', appcast.cli_options[:account])
|
177
168
|
|
178
169
|
App.fail 'Sparkle could not generate keys' unless status.success?
|
179
170
|
|
@@ -181,7 +172,7 @@ module Motion
|
|
181
172
|
puts results.lines[1..].join.indent(11)
|
182
173
|
|
183
174
|
# Extract the public key so we can use it in message
|
184
|
-
results, status = Open3.capture2e(generate_keys_app, '-p')
|
175
|
+
results, status = Open3.capture2e(generate_keys_app, '-p', '--account', appcast.cli_options[:account])
|
185
176
|
|
186
177
|
App.fail 'Unable to read public key' unless status.success?
|
187
178
|
|
@@ -199,7 +190,7 @@ module Motion
|
|
199
190
|
|
200
191
|
# Export the private key from the keychain
|
201
192
|
def export_private_key
|
202
|
-
_results, status = Open3.capture2e(generate_keys_app, '-x', private_key_path.to_s)
|
193
|
+
_results, status = Open3.capture2e(generate_keys_app, '-x', private_key_path.to_s, '--account', appcast.cli_options[:account])
|
203
194
|
|
204
195
|
App.fail 'Unable to export private key' unless status.success?
|
205
196
|
|
@@ -216,6 +207,92 @@ module Motion
|
|
216
207
|
.indent(11)
|
217
208
|
end
|
218
209
|
|
210
|
+
# copy the release notes and zip archive into the releases_folder,
|
211
|
+
# where the appcast will get built
|
212
|
+
def copy_to_release
|
213
|
+
path = (project_path + releases_folder).realpath
|
214
|
+
zip_file_path = (sparkle_release_path + zip_file).realpath
|
215
|
+
|
216
|
+
[release_notes_path, zip_file_path].each do |file|
|
217
|
+
FileUtils.cp(file, "#{path}/")
|
218
|
+
App.info 'Copied', "./#{path}/#{file}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Generate the appcast.
|
223
|
+
# Note: We do not support the old DSA keys, only the newer EdDSA keys.
|
224
|
+
# See https://sparkle-project.org/documentation/eddsa-migration
|
225
|
+
def generate_appcast
|
226
|
+
generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast"
|
227
|
+
path = (project_path + releases_folder).realpath
|
228
|
+
appcast_filename = (path + appcast.feed_filename)
|
229
|
+
appcast.cli_options[:output_path] = appcast_filename
|
230
|
+
|
231
|
+
FileUtils.mkdir_p(path) unless File.exist?(path)
|
232
|
+
|
233
|
+
App.info('Sparkle', "Generating appcast using `#{generate_appcast_app}`")
|
234
|
+
puts "from files in `#{path}`...".indent(11)
|
235
|
+
|
236
|
+
args = appcast.prepare_args
|
237
|
+
|
238
|
+
App.info 'Executing', [generate_appcast_app, *args, path.to_s].join(' ')
|
239
|
+
|
240
|
+
results, status = Open3.capture2e(generate_appcast_app, *args, path.to_s)
|
241
|
+
|
242
|
+
App.info('Sparkle', "Saved appcast to `#{appcast_filename}`") if status.success?
|
243
|
+
puts results.indent(11)
|
244
|
+
|
245
|
+
return unless status.success?
|
246
|
+
|
247
|
+
puts
|
248
|
+
puts "SUFeedURL : #{feed_url}".indent(11)
|
249
|
+
puts "SUPublicEDKey : #{public_ed_dsa_key}".indent(11)
|
250
|
+
end
|
251
|
+
|
252
|
+
def generate_appcast_help
|
253
|
+
generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast"
|
254
|
+
results, _status = Open3.capture2e(generate_appcast_app, '--help')
|
255
|
+
|
256
|
+
puts results
|
257
|
+
end
|
258
|
+
|
259
|
+
def create_release_notes
|
260
|
+
App.fail "Release notes template not found as expected at ./#{release_notes_template_path}" unless File.exist?(release_notes_template_path)
|
261
|
+
|
262
|
+
create_release_folder
|
263
|
+
|
264
|
+
File.open(release_notes_path.to_s, 'w') do |f|
|
265
|
+
template = File.read(release_notes_template_path)
|
266
|
+
f << ERB.new(template).result(binding)
|
267
|
+
end
|
268
|
+
|
269
|
+
App.info 'Create', "./#{release_notes_path}"
|
270
|
+
end
|
271
|
+
|
272
|
+
def release_notes_template_path
|
273
|
+
sparkle_config_path.join('release_notes.template.erb')
|
274
|
+
end
|
275
|
+
|
276
|
+
def release_notes_content_path
|
277
|
+
sparkle_config_path.join('release_notes.content.html')
|
278
|
+
end
|
279
|
+
|
280
|
+
def release_notes_path
|
281
|
+
sparkle_release_path + (appcast.notes_filename || "#{app_name}.#{@config.short_version}.html")
|
282
|
+
end
|
283
|
+
|
284
|
+
def release_notes_content
|
285
|
+
if File.exist?(release_notes_content_path)
|
286
|
+
File.read(release_notes_content_path)
|
287
|
+
else
|
288
|
+
App.fail "Missing #{release_notes_content_path}"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def release_notes_html
|
293
|
+
release_notes_content
|
294
|
+
end
|
295
|
+
|
219
296
|
# A few helpers
|
220
297
|
|
221
298
|
def project_path
|
@@ -242,10 +319,6 @@ module Motion
|
|
242
319
|
sparkle_config_path.join(EDDSA_PRIV_KEY)
|
243
320
|
end
|
244
321
|
|
245
|
-
def legacy_private_key_path
|
246
|
-
sparkle_config_path.join(DSA_PRIV_KEY)
|
247
|
-
end
|
248
|
-
|
249
322
|
def app_bundle_path
|
250
323
|
Pathname.new(@config.app_bundle_raw('MacOSX'))
|
251
324
|
end
|
@@ -262,8 +335,8 @@ module Motion
|
|
262
335
|
appcast.package_filename || "#{app_name}.#{@config.short_version}.zip"
|
263
336
|
end
|
264
337
|
|
265
|
-
def
|
266
|
-
appcast.
|
338
|
+
def releases_folder
|
339
|
+
appcast.releases_folder
|
267
340
|
end
|
268
341
|
|
269
342
|
def app_file
|
data/sample-app/.gitignore
CHANGED
data/sample-app/Rakefile
CHANGED
@@ -25,8 +25,8 @@ Motion::Project::App.setup do |app|
|
|
25
25
|
|
26
26
|
app.sparkle do
|
27
27
|
publish :base_url, 'http://example.com/your_app_folder/releases/'
|
28
|
-
publish :public_key, '
|
29
|
-
publish :
|
28
|
+
publish :public_key, '1/eJupmw4JH+iFQIlh99nS2qvwYealJsNzFN3LL6FFE=x'
|
29
|
+
publish :releases_folder, 'tmp/releases/v2.2/'
|
30
30
|
|
31
31
|
# Appcast Feed
|
32
32
|
# publish :feed_base_url, 'http://downloads.example.com/releases' # defaults to base_url
|
@@ -40,5 +40,8 @@ Motion::Project::App.setup do |app|
|
|
40
40
|
# publish :package_base_url, 'http://downloads.example.com/releases/' # defaults to base_url
|
41
41
|
|
42
42
|
# publish :use_exported_private_key, true
|
43
|
+
|
44
|
+
publish :account, 'motion-sparkle-sandbox.sample-app'
|
45
|
+
publish :full_release_notes_url, 'http://example.com/full_release_notes_changed_again.html'
|
43
46
|
end
|
44
47
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('spec_utils', __dir__)
|
4
|
+
|
5
|
+
describe 'Appcast' do
|
6
|
+
describe 'arguments' do
|
7
|
+
before do
|
8
|
+
@appcast = Motion::Project::Sparkle::Appcast.new(nil)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'defaults to empty' do
|
12
|
+
args = @appcast.prepare_args
|
13
|
+
|
14
|
+
expect(args).to eq []
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'sets all command line options' do
|
18
|
+
expected_results = [
|
19
|
+
'--account=test1',
|
20
|
+
'-s=test2',
|
21
|
+
'--download-url-prefix=test3',
|
22
|
+
'--release-notes-url-prefix=test4',
|
23
|
+
'--full-release-notes-url=test5',
|
24
|
+
'--link=test6',
|
25
|
+
'--versions=test7',
|
26
|
+
'--maximum-deltas=test8',
|
27
|
+
'--delta-compression=test9',
|
28
|
+
'--delta-compression-level=test10',
|
29
|
+
'--channel=test11',
|
30
|
+
'--major-version=test12',
|
31
|
+
'--ignore-skipped-upgrades-below-version=test13',
|
32
|
+
'--phased-rollout-interval=test14',
|
33
|
+
'--critical-update-version=test15',
|
34
|
+
'--informational-update-versions=test16',
|
35
|
+
'-o=test17'
|
36
|
+
]
|
37
|
+
|
38
|
+
@appcast.process_option(:account, 'test1')
|
39
|
+
@appcast.process_option(:private_eddsa_key, 'test2')
|
40
|
+
@appcast.process_option(:download_url_prefix, 'test3')
|
41
|
+
@appcast.process_option(:release_notes_url_prefix, 'test4')
|
42
|
+
@appcast.process_option(:full_release_notes_url, 'test5')
|
43
|
+
@appcast.process_option(:link, 'test6')
|
44
|
+
@appcast.process_option(:versions, 'test7')
|
45
|
+
@appcast.process_option(:maximum_deltas, 'test8')
|
46
|
+
@appcast.process_option(:delta_compression, 'test9')
|
47
|
+
@appcast.process_option(:delta_compression_level, 'test10')
|
48
|
+
@appcast.process_option(:channel, 'test11')
|
49
|
+
@appcast.process_option(:major_version, 'test12')
|
50
|
+
@appcast.process_option(:ignore_skipped_upgrades_below_version, 'test13')
|
51
|
+
@appcast.process_option(:phased_rollout_interval, 'test14')
|
52
|
+
@appcast.process_option(:critical_update_version, 'test15')
|
53
|
+
@appcast.process_option(:informational_update_versions, 'test16')
|
54
|
+
@appcast.process_option(:output_path, 'test17')
|
55
|
+
args = @appcast.prepare_args
|
56
|
+
|
57
|
+
expect(args).to match_array expected_results
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/sparkle_spec.rb
CHANGED
@@ -29,6 +29,7 @@ describe 'motion-sparkle-sandbox' do
|
|
29
29
|
|
30
30
|
it 'uses feed_base_url' do
|
31
31
|
@config.sparkle.publish(:feed_base_url, 'http://rss.example.com/')
|
32
|
+
@config.sparkle.after_initialize
|
32
33
|
|
33
34
|
expect(@config.info_plist['SUFeedURL']).to eq 'http://rss.example.com/releases.xml'
|
34
35
|
end
|
@@ -36,6 +37,7 @@ describe 'motion-sparkle-sandbox' do
|
|
36
37
|
it 'uses feed_filename' do
|
37
38
|
@config.sparkle.publish(:feed_base_url, 'http://rss.example.com/')
|
38
39
|
@config.sparkle.publish(:feed_filename, 'example.xml')
|
40
|
+
@config.sparkle.after_initialize
|
39
41
|
|
40
42
|
expect(@config.info_plist['SUFeedURL']).to eq 'http://rss.example.com/example.xml'
|
41
43
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: motion-sparkle-sandbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Walker
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-06-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: motion-cocoapods
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- sample-app/resources/Assets.xcassets/Contents.json
|
96
96
|
- sample-app/resources/Credits.rtf
|
97
97
|
- sample-app/spec/main_spec.rb
|
98
|
+
- spec/appcast_spec.rb
|
98
99
|
- spec/setup_spec.rb
|
99
100
|
- spec/sparkle_spec.rb
|
100
101
|
- spec/spec_helper.rb
|
@@ -124,6 +125,7 @@ signing_key:
|
|
124
125
|
specification_version: 4
|
125
126
|
summary: Sparkle (sandboxed) integration for Rubymotion projects
|
126
127
|
test_files:
|
128
|
+
- spec/appcast_spec.rb
|
127
129
|
- spec/spec_helper.rb
|
128
130
|
- spec/sparkle_spec.rb
|
129
131
|
- spec/setup_spec.rb
|