Class: Motion::Project::Sparkle
- Inherits:
-
Object
- Object
- Motion::Project::Sparkle
- Defined in:
- lib/motion/project/sparkle.rb,
lib/motion/project/setup.rb,
lib/motion/project/appcast.rb,
lib/motion/project/install.rb,
lib/motion/project/package.rb,
lib/motion/project/templates.rb
Overview
rubocop:disable Metrics/ClassLength
Defined Under Namespace
Classes: Appcast
Constant Summary collapse
- SPARKLE_ROOT =
'sparkle'
- CONFIG_PATH =
"#{SPARKLE_ROOT}/config"
- RELEASE_PATH =
"#{SPARKLE_ROOT}/release"
- EDDSA_PRIV_KEY =
'eddsa_priv.key'
- DSA_PRIV_KEY =
'dsa_priv.pem'
- TEMPLATE_PATHS =
[ File.(File.join(__FILE__, '../appcast')) ].freeze
Instance Method Summary collapse
-
#add_to_gitignore ⇒ Object
File manipulation and certificates.
- #after_initialize ⇒ Object
- #all_templates ⇒ Object
- #app_bundle_path ⇒ Object
- #app_file ⇒ Object
- #app_name ⇒ Object
- #app_release_path ⇒ Object
- #appcast ⇒ Object
- #certificates_ok?(silence = false) ⇒ Boolean
- #check_base_url ⇒ Object
- #check_feed_url ⇒ Object
- #check_public_key ⇒ Object
- #config_ok? ⇒ Boolean
- #copy_templates(force = false) ⇒ Object
-
#copy_to_release ⇒ Object
copy the release notes and zip archive into the releases_folder, where the appcast will get built.
-
#create_private_key ⇒ Object
Create the private key in the keychain.
- #create_release_notes ⇒ Object
- #create_sparkle_folder ⇒ Object
- #create_zip_file ⇒ Object
-
#export_private_key ⇒ Object
Export the private key from the keychain.
- #feed_url ⇒ Object
- #feed_url=(url) ⇒ Object
-
#generate_appcast ⇒ Object
Generate the appcast.
- #generate_appcast_help ⇒ Object
-
#generate_keys ⇒ Object
rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength.
- #generate_keys_app ⇒ Object
- #gitignore_path ⇒ Object
-
#initialize(config) ⇒ Sparkle
constructor
A new instance of Sparkle.
- #installed? ⇒ Boolean
- #package ⇒ Object
- #private_key_path ⇒ Object
-
#project_path ⇒ Object
A few helpers.
- #public_ed_dsa_key ⇒ Object
- #public_ed_dsa_key=(key) ⇒ Object
- #publish(key, value) ⇒ Object (also: #release)
- #release_notes_content ⇒ Object
- #release_notes_content_path ⇒ Object
- #release_notes_html ⇒ Object
- #release_notes_path ⇒ Object
- #release_notes_template_path ⇒ Object
- #releases_folder ⇒ Object
- #setup ⇒ Object
- #setup_ok? ⇒ Boolean
- #sign_package ⇒ Object
- #sparkle_config_path ⇒ Object
- #sparkle_release_path ⇒ Object
- #vendor_path ⇒ Object
- #vendored_sparkle_framework_path ⇒ Object
- #vendored_sparkle_path ⇒ Object
- #vendored_sparkle_xpc_path ⇒ Object
- #verify_installation ⇒ Object
- #version(vstring) ⇒ Object
- #version_string ⇒ Object
- #zip_file ⇒ Object
Constructor Details
#initialize(config) ⇒ Sparkle
Returns a new instance of Sparkle.
13 14 15 16 |
# File 'lib/motion/project/sparkle.rb', line 13 def initialize(config) @config = config # verify_installation end |
Instance Method Details
#add_to_gitignore ⇒ Object
File manipulation and certificates
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/motion/project/sparkle.rb', line 67 def add_to_gitignore @ignorable = ['sparkle/release', 'sparkle/release/*', private_key_path] return unless File.exist?(gitignore_path) File.open(gitignore_path, 'r') do |f| f.each_line do |line| @ignorable.delete(line) if @ignorable.include?(line) end end if @ignorable.any? File.open(gitignore_path, 'a') do |f| @ignorable.each do |i| f << "#{i}\n" end end end `cat #{gitignore_path}` end |
#after_initialize ⇒ Object
18 19 20 |
# File 'lib/motion/project/sparkle.rb', line 18 def after_initialize self.feed_url = appcast.feed_url end |
#all_templates ⇒ Object
13 14 15 16 17 18 19 20 21 |
# File 'lib/motion/project/templates.rb', line 13 def all_templates @all_templates ||= begin templates = {} TEMPLATE_PATHS.map { |path| Dir.glob("#{path}/*") }.flatten.each do |template_path| templates[File.basename(template_path)] = template_path end templates end end |
#app_bundle_path ⇒ Object
322 323 324 |
# File 'lib/motion/project/sparkle.rb', line 322 def app_bundle_path Pathname.new(@config.app_bundle_raw('MacOSX')) end |
#app_file ⇒ Object
342 343 344 |
# File 'lib/motion/project/sparkle.rb', line 342 def app_file "#{app_name}.app" end |
#app_name ⇒ Object
330 331 332 |
# File 'lib/motion/project/sparkle.rb', line 330 def app_name File.basename(app_bundle_path, '.app') end |
#app_release_path ⇒ Object
326 327 328 |
# File 'lib/motion/project/sparkle.rb', line 326 def app_release_path app_bundle_path.parent.to_s end |
#appcast ⇒ Object
22 23 24 |
# File 'lib/motion/project/sparkle.rb', line 22 def appcast @appcast ||= Appcast.new(self) end |
#certificates_ok?(silence = false) ⇒ Boolean
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/motion/project/setup.rb', line 35 def certificates_ok?(silence = false) unless File.exist?("./#{Sparkle::CONFIG_PATH}") return false if silence App.fail "Missing `#{Sparkle::CONFIG_PATH}`. Run `rake sparkle:setup` to get started" end if appcast.use_exported_private_key && !File.exist?(private_key_path) return false if silence App.fail "Missing `#{private_key_path}`. Please run `rake sparkle:setup_certificates` or check the docs to know where to put them." end unless public_ed_dsa_key.present? return false if silence 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`" end true end |
#check_base_url ⇒ Object
17 18 19 20 21 |
# File 'lib/motion/project/setup.rb', line 17 def check_base_url return true if appcast.base_url.present? App.fail "Sparkle :base_url missing. Use `release :base_url, 'http://example.com/your_app_folder'` in your Rakefile's `app.sparkle` block" end |
#check_feed_url ⇒ Object
23 24 25 26 27 |
# File 'lib/motion/project/setup.rb', line 23 def check_feed_url return true if feed_url.present? && appcast.feed_filename.present? App.fail 'Sparkle :feed_filename is nil or blank. Please check your Rakefile.' end |
#check_public_key ⇒ Object
29 30 31 32 33 |
# File 'lib/motion/project/setup.rb', line 29 def check_public_key return true if public_ed_dsa_key.present? App.fail 'Sparkle :public_key is nil or blank. Please check your Rakefile.' end |
#config_ok? ⇒ Boolean
11 12 13 14 15 |
# File 'lib/motion/project/setup.rb', line 11 def config_ok? check_base_url check_feed_url check_public_key end |
#copy_templates(force = false) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/motion/project/templates.rb', line 23 def copy_templates(force = false) all_templates.each_pair do |tmpl, path| result = "#{sparkle_config_path}/#{tmpl}" if File.exist?(result) && !force App.info 'Exists', result else FileUtils.cp(path, "#{sparkle_config_path}/") App.info 'Create', "./#{sparkle_config_path}/#{tmpl}" end end end |
#copy_to_release ⇒ Object
copy the release notes and zip archive into the releases_folder, where the appcast will get built
212 213 214 215 216 217 218 219 220 |
# File 'lib/motion/project/sparkle.rb', line 212 def copy_to_release destination_path = (project_path + releases_folder).realpath zip_file_path = (sparkle_release_path + zip_file) [release_notes_path, zip_file_path].each do |file| FileUtils.cp(file, "#{destination_path}/") App.info 'Copied', "#{destination_path}/#{file}" end end |
#create_private_key ⇒ Object
Create the private key in the keychain
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/motion/project/sparkle.rb', line 164 def create_private_key App.info 'Sparkle', 'Generating a new signing key into the Keychain. This may take a moment, depending on your machine.' results, status = Open3.capture2e(generate_keys_app, '--account', appcast.[:account]) App.fail 'Sparkle could not generate keys' unless status.success? puts puts results.lines[1..].join.indent(11) # Extract the public key so we can use it in message results, status = Open3.capture2e(generate_keys_app, '-p', '--account', appcast.[:account]) App.fail 'Unable to read public key' unless status.success? puts <<~KEYS You can easily add the `SUPublicEDKey` by publishing the key in your Rakefile: app.sparkle do ... publish :public_key, '#{results.strip}' end KEYS .indent(11) end |
#create_release_notes ⇒ Object
259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/motion/project/sparkle.rb', line 259 def create_release_notes App.fail "Release notes template not found as expected at ./#{release_notes_template_path}" unless File.exist?(release_notes_template_path) create_release_folder File.open(release_notes_path.to_s, 'w') do |f| template = File.read(release_notes_template_path) f << ERB.new(template).result(binding) end App.info 'Create', "./#{release_notes_path}" end |
#create_sparkle_folder ⇒ Object
86 87 88 89 |
# File 'lib/motion/project/sparkle.rb', line 86 def create_sparkle_folder create_config_folder create_release_folder end |
#create_zip_file ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/motion/project/package.rb', line 24 def create_zip_file App.fail 'You need to build your app with the Release target to use Sparkle' unless File.exist?(app_bundle_path) App.info 'Create', "./#{sparkle_release_path}/#{zip_file}" if File.exist?("#{sparkle_release_path}/#{zip_file}") App.fail "Release already exists at ./#{sparkle_release_path}/#{zip_file} (remove it manually with `rake sparkle:clean`)" end FileUtils.cd(app_release_path) do `zip -r --symlinks "#{zip_file}" "#{app_file}"` end FileUtils.mv "#{app_release_path}/#{zip_file}", "./#{sparkle_release_path}/" @package_file = zip_file @package_size = File.size "./#{sparkle_release_path}/#{zip_file}" end |
#export_private_key ⇒ Object
Export the private key from the keychain
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/motion/project/sparkle.rb', line 192 def export_private_key _results, status = Open3.capture2e(generate_keys_app, '-x', private_key_path.to_s, '--account', appcast.[:account]) App.fail 'Unable to export private key' unless status.success? App.info 'Sparkle', 'Private key has been exported from the keychain into the file:' puts <<~KEYS ./#{private_key_path} ADD THIS PRIVATE KEY TO YOUR `.gitignore` OR EQUIVALENT AND BACK IT UP! KEEP IT PRIVATE AND SAFE! If you lose it, your users will be unable to upgrade, unless you used Apple code signing. See https://sparkle-project.org/documentation/ for details KEYS .indent(11) end |
#feed_url ⇒ Object
49 50 51 |
# File 'lib/motion/project/sparkle.rb', line 49 def feed_url @config.info_plist['SUFeedURL'] end |
#feed_url=(url) ⇒ Object
53 54 55 |
# File 'lib/motion/project/sparkle.rb', line 53 def feed_url=(url) @config.info_plist['SUFeedURL'] = url end |
#generate_appcast ⇒ Object
Generate the appcast. Note: We do not support the old DSA keys, only the newer EdDSA keys.
See https://sparkle-project.org/documentation/eddsa-migration
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/motion/project/sparkle.rb', line 225 def generate_appcast generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast" path = (project_path + releases_folder).realpath appcast_filename = (path + appcast.feed_filename) appcast.[:output_path] = appcast_filename FileUtils.mkdir_p(path) unless File.exist?(path) App.info('Sparkle', "Generating appcast using `#{generate_appcast_app}`") puts "from files in `#{path}`...".indent(11) args = appcast.prepare_args App.info 'Executing', [generate_appcast_app, *args, path.to_s].join(' ') results, status = Open3.capture2e(generate_appcast_app, *args, path.to_s) App.info('Sparkle', "Saved appcast to `#{appcast_filename}`") if status.success? puts results.indent(11) return unless status.success? puts puts "SUFeedURL : #{feed_url}".indent(11) puts "SUPublicEDKey : #{public_ed_dsa_key}".indent(11) end |
#generate_appcast_help ⇒ Object
252 253 254 255 256 257 |
# File 'lib/motion/project/sparkle.rb', line 252 def generate_appcast_help generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast" results, _status = Open3.capture2e(generate_appcast_app, '--help') puts results end |
#generate_keys ⇒ Object
rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/motion/project/sparkle.rb', line 96 def generate_keys return false unless config_ok? FileUtils.mkdir_p sparkle_config_path unless File.exist?(sparkle_config_path) if appcast.use_exported_private_key && File.exist?(private_key_path) App.info 'Sparkle', "Private key already exported at `#{private_key_path}` and will be used." if public_ed_dsa_key.present? App.info '', <<~EXISTS SUPublicEDKey already set Be careful not to override or lose your certificates. Delete this file if you're sure. Aborting (no action performed) EXISTS .indent(11, skip_first_line: true) else App.info '', <<~EXISTS SUPublicEDKey NOT SET You can easily add the `SUPublicEDKey` by publishing the key in your Rakefile: app.sparkle do ... publish :public_key, 'PUBLIC_KEY' end Be careful not to override or lose your certificates. Delete this file if you're sure. Aborting (no action performed) EXISTS .indent(11, skip_first_line: true) end return end results, status = Open3.capture2e(generate_keys_app, '-p', '--account', appcast.[:account]) if status.success? App.info 'Sparkle', "Public/private keys found in the keychain for account #{appcast.[:account]}" if results.strip == public_ed_dsa_key App.info 'Sparkle', 'Keychain public key matches `SUPublicEDKey`' if appcast.use_exported_private_key && !File.exist?(private_key_path) # export the private key from the keychain end else App.fail <<~NOT_MATCHED Keychain public key DOES NOT match `SUPublicEDKey` Keychain public key: #{results.strip} SUPublicEDKey public key: #{public_ed_dsa_key} NOT_MATCHED .indent(11, skip_first_line: true) end return end create_private_key export_private_key if appcast.use_exported_private_key end |
#generate_keys_app ⇒ Object
91 92 93 |
# File 'lib/motion/project/sparkle.rb', line 91 def generate_keys_app "#{vendored_sparkle_path}/bin/generate_keys" end |
#gitignore_path ⇒ Object
306 307 308 |
# File 'lib/motion/project/sparkle.rb', line 306 def gitignore_path project_path.join('.gitignore') end |
#installed? ⇒ Boolean
18 19 20 |
# File 'lib/motion/project/install.rb', line 18 def installed? File.directory?(vendored_sparkle_framework_path) end |
#package ⇒ Object
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# File 'lib/motion/project/package.rb', line 6 def package return unless setup_ok? create_release_folder @config.build_mode = :release return unless create_zip_file App.info 'Release', version_string App.info 'Version', @config.short_version App.info 'Build', @config.version || 'unspecified in Rakefile' App.info 'Size', @package_size.to_s sign_package create_release_notes `open #{sparkle_release_path}` end |
#private_key_path ⇒ Object
318 319 320 |
# File 'lib/motion/project/sparkle.rb', line 318 def private_key_path sparkle_config_path.join(EDDSA_PRIV_KEY) end |
#project_path ⇒ Object
A few helpers
298 299 300 |
# File 'lib/motion/project/sparkle.rb', line 298 def project_path @project_path ||= Pathname.new(@config.project_dir) end |
#public_ed_dsa_key ⇒ Object
57 58 59 |
# File 'lib/motion/project/sparkle.rb', line 57 def public_ed_dsa_key @config.info_plist['SUPublicEDKey'] end |
#public_ed_dsa_key=(key) ⇒ Object
61 62 63 |
# File 'lib/motion/project/sparkle.rb', line 61 def public_ed_dsa_key=(key) @config.info_plist['SUPublicEDKey'] = key end |
#publish(key, value) ⇒ Object Also known as: release
26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/motion/project/sparkle.rb', line 26 def publish(key, value) return if appcast.process_option(key, value) case key when :public_key self.public_ed_dsa_key = value when :version version(value) else raise "Unknown Sparkle config option #{key}" end end |
#release_notes_content ⇒ Object
284 285 286 287 288 289 290 |
# File 'lib/motion/project/sparkle.rb', line 284 def release_notes_content if File.exist?(release_notes_content_path) File.read(release_notes_content_path) else App.fail "Missing #{release_notes_content_path}" end end |
#release_notes_content_path ⇒ Object
276 277 278 |
# File 'lib/motion/project/sparkle.rb', line 276 def release_notes_content_path sparkle_config_path.join('release_notes.content.html') end |
#release_notes_html ⇒ Object
292 293 294 |
# File 'lib/motion/project/sparkle.rb', line 292 def release_notes_html release_notes_content end |
#release_notes_path ⇒ Object
280 281 282 |
# File 'lib/motion/project/sparkle.rb', line 280 def release_notes_path sparkle_release_path + (appcast.notes_filename || "#{app_name}.#{@config.short_version}.html") end |
#release_notes_template_path ⇒ Object
272 273 274 |
# File 'lib/motion/project/sparkle.rb', line 272 def release_notes_template_path sparkle_config_path.join('release_notes.template.erb') end |
#releases_folder ⇒ Object
338 339 340 |
# File 'lib/motion/project/sparkle.rb', line 338 def releases_folder appcast.releases_folder end |
#setup ⇒ Object
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/motion/project/setup.rb', line 57 def setup verify_installation create_sparkle_folder add_to_gitignore copy_templates return false unless config_ok? App.info 'Sparkle', 'Config found' silence = true unless certificates_ok?(silence) App.info 'Sparkle', <<~CERTIFICATES Certificates not found Please generate your private and public keys with `rake sparkle:setup_certificates` If you already have your certificates and only need to include them in the project, follow these steps: 1. Rename your private key to `./#{private_key_path}` and make sure you've added it to your `.gitignore` file - it should NEVER be stored in your repository 2. Add `publish :public_key, 'PUBLIC_KEY'` to the Sparkle config in your Rakefile CERTIFICATES return false end App.info 'Sparkle', 'Certificates found' App.info 'Sparkle', 'Setup OK. After `rake build:release`, you can now run `rake sparkle:package`.' end |
#setup_ok? ⇒ Boolean
6 7 8 9 |
# File 'lib/motion/project/setup.rb', line 6 def setup_ok? config_ok? certificates_ok? end |
#sign_package ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/motion/project/package.rb', line 43 def sign_package package = "./#{sparkle_release_path}/#{zip_file}" sign_update_app = "#{vendored_sparkle_path}/bin/sign_update" args = [] if appcast.use_exported_private_key && File.exist?(private_key_path) # -s <private-key> The private EdDSA (ed25519) key private_key = File.read(private_key_path) args << "-s=#{private_key}" end results, _status = Open3.capture2e(sign_update_app, *args, package) App.info 'Signature', results end |
#sparkle_config_path ⇒ Object
314 315 316 |
# File 'lib/motion/project/sparkle.rb', line 314 def sparkle_config_path project_path.join(CONFIG_PATH) end |
#sparkle_release_path ⇒ Object
310 311 312 |
# File 'lib/motion/project/sparkle.rb', line 310 def sparkle_release_path project_path.join(RELEASE_PATH) end |
#vendor_path ⇒ Object
302 303 304 |
# File 'lib/motion/project/sparkle.rb', line 302 def vendor_path @vendor_path ||= project_path.join('vendor') end |
#vendored_sparkle_framework_path ⇒ Object
10 11 12 |
# File 'lib/motion/project/install.rb', line 10 def vendored_sparkle_framework_path vendored_sparkle_path.join('Sparkle.framework') end |
#vendored_sparkle_path ⇒ Object
6 7 8 |
# File 'lib/motion/project/install.rb', line 6 def vendored_sparkle_path vendor_path.join('Pods/Sparkle') end |
#vendored_sparkle_xpc_path ⇒ Object
14 15 16 |
# File 'lib/motion/project/install.rb', line 14 def vendored_sparkle_xpc_path vendored_sparkle_path.join('XPCServices') end |
#verify_installation ⇒ Object
22 23 24 25 26 27 28 |
# File 'lib/motion/project/install.rb', line 22 def verify_installation if installed? App.info 'Sparkle', "Framework installed in #{vendored_sparkle_framework_path}" else App.fail "Sparkle Cocoapod not correctly installed to #{vendored_sparkle_path}. Run `rake pod:install`." end end |
#version(vstring) ⇒ Object
40 41 42 43 |
# File 'lib/motion/project/sparkle.rb', line 40 def version(vstring) @config.version = vstring.to_s @config.short_version = vstring.to_s end |
#version_string ⇒ Object
45 46 47 |
# File 'lib/motion/project/sparkle.rb', line 45 def version_string "#{@config.short_version} (#{@config.version})" end |
#zip_file ⇒ Object
334 335 336 |
# File 'lib/motion/project/sparkle.rb', line 334 def zip_file appcast.package_filename || "#{app_name}.#{@config.short_version}.zip" end |