Skip to content

Commit dbeab5f

Browse files
committed
Refactor Appcast
- add support for additional CLI options - add copy_to_release rake task
1 parent 3e50389 commit dbeab5f

File tree

10 files changed

+431
-167
lines changed

10 files changed

+431
-167
lines changed

lib/motion-sparkle-sandbox/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module MotionSparkleSandbox
4-
VERSION = '2.1.0'
4+
VERSION = '2.1.1'
55
end

lib/motion/project/appcast.rb

+237-127
Large diffs are not rendered by default.

lib/motion/project/project.rb

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Config
1010
def sparkle(&block)
1111
@sparkle ||= Motion::Project::Sparkle.new(self)
1212
@sparkle.instance_eval(&block) if block
13+
@sparkle.after_initialize
1314
@sparkle
1415
end
1516
end

lib/motion/project/rake_tasks.rb

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
# frozen_string_literal: true
22

33
# Sparkle specific rake tasks
4+
# rubocop:disable Metrics/BlockLength
45
namespace :sparkle do
56
desc 'Sparkle Help'
67
task :help do
78
puts <<~HELP
9+
10+
811
During initial Sparkle setup, run these rake tasks:
912
1013
1. `rake sparkle:setup_certificates`
1114
2. `rake sparkle:setup`
1215
1316
Then after running `rake build:release`, you can run
14-
`rake sparkle:package`
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
1521
HELP
1622
end
1723

@@ -40,6 +46,13 @@
4046
sparkle.sign_package
4147
end
4248

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+
4356
desc "Generate the appcast xml feed using Sparkle's `generate_appcast`"
4457
task :generate_appcast do
4558
App.config_without_setup.build_mode = :release
@@ -83,3 +96,4 @@
8396
end
8497
end
8598
end
99+
# rubocop:enable Metrics/BlockLength

lib/motion/project/setup.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def check_feed_url
2727
end
2828

2929
def check_public_key
30-
return true if public_EdDSA_key.present?
30+
return true if public_ed_dsa_key.present?
3131

3232
App.fail 'Sparkle :public_key is nil or blank. Please check your Rakefile.'
3333
end
@@ -45,7 +45,7 @@ def certificates_ok?(silence = false)
4545
App.fail "Missing `#{private_key_path}`. Please run `rake sparkle:setup_certificates` or check the docs to know where to put them."
4646
end
4747

48-
unless public_EdDSA_key.present?
48+
unless public_ed_dsa_key.present?
4949
return false if silence
5050

5151
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`"

lib/motion/project/sparkle.rb

+107-34
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,22 @@ def initialize(config)
1515
# verify_installation
1616
end
1717

18+
def after_initialize
19+
self.feed_url = appcast.feed_url
20+
end
21+
1822
def appcast
19-
@appcast ||= Appcast.new
23+
@appcast ||= Appcast.new(self)
2024
end
2125

2226
def publish(key, value)
27+
return if appcast.process_option(key, value)
28+
2329
case key
2430
when :public_key
25-
self.public_EdDSA_key = value
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
3532
when :version
36-
version value
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)
4134
else
4235
raise "Unknown Sparkle config option #{key}"
4336
end
@@ -61,15 +54,13 @@ def feed_url=(url)
6154
@config.info_plist['SUFeedURL'] = url
6255
end
6356

64-
# rubocop:disable Naming/MethodName
65-
def public_EdDSA_key
57+
def public_ed_dsa_key
6658
@config.info_plist['SUPublicEDKey']
6759
end
6860

69-
def public_EdDSA_key=(key)
61+
def public_ed_dsa_key=(key)
7062
@config.info_plist['SUPublicEDKey'] = key
7163
end
72-
# rubocop:enable Naming/MethodName
7364

7465
# File manipulation and certificates
7566

@@ -109,7 +100,7 @@ def generate_keys
109100

110101
if appcast.use_exported_private_key && File.exist?(private_key_path)
111102
App.info 'Sparkle', "Private key already exported at `#{private_key_path}` and will be used."
112-
if public_EdDSA_key.present?
103+
if public_ed_dsa_key.present?
113104
App.info '', <<~EXISTS
114105
SUPublicEDKey already set
115106
@@ -139,12 +130,12 @@ def generate_keys
139130
return
140131
end
141132

142-
results, status = Open3.capture2e(generate_keys_app, '-p')
133+
results, status = Open3.capture2e(generate_keys_app, '-p', '--account', appcast.cli_options[:account])
143134

144135
if status.success?
145-
App.info 'Sparkle', 'Public/private keys found in the keychain'
136+
App.info 'Sparkle', "Public/private keys found in the keychain for account #{appcast.cli_options[:account]}"
146137

147-
if results.strip == public_EdDSA_key
138+
if results.strip == public_ed_dsa_key
148139
App.info 'Sparkle', 'Keychain public key matches `SUPublicEDKey`'
149140

150141
if appcast.use_exported_private_key && !File.exist?(private_key_path)
@@ -155,7 +146,7 @@ def generate_keys
155146
Keychain public key DOES NOT match `SUPublicEDKey`
156147
157148
Keychain public key: #{results.strip}
158-
SUPublicEDKey public key: #{public_EdDSA_key}
149+
SUPublicEDKey public key: #{public_ed_dsa_key}
159150
160151
NOT_MATCHED
161152
.indent(11, skip_first_line: true)
@@ -173,15 +164,15 @@ def generate_keys
173164
def create_private_key
174165
App.info 'Sparkle',
175166
'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])
177168

178169
App.fail 'Sparkle could not generate keys' unless status.success?
179170

180171
puts
181172
puts results.lines[1..].join.indent(11)
182173

183174
# 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])
185176

186177
App.fail 'Unable to read public key' unless status.success?
187178

@@ -199,7 +190,7 @@ def create_private_key
199190

200191
# Export the private key from the keychain
201192
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])
203194

204195
App.fail 'Unable to export private key' unless status.success?
205196

@@ -216,6 +207,92 @@ def export_private_key
216207
.indent(11)
217208
end
218209

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+
219296
# A few helpers
220297

221298
def project_path
@@ -242,10 +319,6 @@ def private_key_path
242319
sparkle_config_path.join(EDDSA_PRIV_KEY)
243320
end
244321

245-
def legacy_private_key_path
246-
sparkle_config_path.join(DSA_PRIV_KEY)
247-
end
248-
249322
def app_bundle_path
250323
Pathname.new(@config.app_bundle_raw('MacOSX'))
251324
end
@@ -262,8 +335,8 @@ def zip_file
262335
appcast.package_filename || "#{app_name}.#{@config.short_version}.zip"
263336
end
264337

265-
def archive_folder
266-
appcast.archive_folder
338+
def releases_folder
339+
appcast.releases_folder
267340
end
268341

269342
def app_file

sample-app/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ vendor
77

88
sparkle/release
99
sparkle/config
10+
sparkle/config/eddsa_priv.key

sample-app/Rakefile

+5-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ Motion::Project::App.setup do |app|
2525

2626
app.sparkle do
2727
publish :base_url, 'http://example.com/your_app_folder/releases/'
28-
publish :public_key, '06Iq4HGJFToJrA8lqWWG0SCqqtGXLuEN1Wgy/CVJBnI='
29-
publish :archive_folder, '../published/releases/v2.2/'
28+
publish :public_key, '1/eJupmw4JH+iFQIlh99nS2qvwYealJsNzFN3LL6FFE=x'
29+
publish :releases_folder, 'tmp/releases/v2.2/'
3030

3131
# Appcast Feed
3232
# publish :feed_base_url, 'http://downloads.example.com/releases' # defaults to base_url
@@ -40,5 +40,8 @@ Motion::Project::App.setup do |app|
4040
# publish :package_base_url, 'http://downloads.example.com/releases/' # defaults to base_url
4141

4242
# 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'
4346
end
4447
end

spec/appcast_spec.rb

+60
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)