motion-sparkle-sandbox 2.1.0 → 2.1.1
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 +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
|