lib/motion/project/appcast.rb in motion-sparkle-sandbox-2.0.0 vs lib/motion/project/appcast.rb in motion-sparkle-sandbox-2.0.1
- old
+ new
@@ -1,127 +1,200 @@
-module Motion::Project
- class Sparkle
+# frozen_string_literal: true
- def create_release_notes
- if File.exist?(release_notes_template_path)
- File.open("#{release_notes_path}", "w") do |f|
- template = File.read(release_notes_template_path)
- f << ERB.new(template).result(binding)
+module Motion
+ module Project
+ class Sparkle
+ # 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
+ # rubocop:disable Metrics/CyclomaticComplexity
+ def generate_appcast
+ generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast"
+ path = (project_path + archive_folder).realpath
+ appcast_filename = (path + appcast.feed_filename)
+
+ args = []
+
+ FileUtils.mkdir_p(path) unless File.exist?(path)
+
+ App.info('Sparkle', "Generating appcast using `#{generate_appcast_app}`")
+ puts "from files in `#{path}`...".indent(11)
+
+ if appcast.use_exported_private_key && File.exist?(private_key_path)
+ # -s <private-EdDSA-key> The private EdDSA string (128 characters). If not
+ # specified, the private EdDSA key will be read from
+ # the Keychain instead.
+ private_key = File.read(private_key_path)
+ args << "-s=#{private_key}"
end
- App.info 'Create', "./#{release_notes_path}"
- else
- App.fail "Release notes template not found as expected at ./#{release_notes_template_path}"
- end
- end
- def create_appcast
- create_release_folder
- appcast_file = File.open("#{sparkle_release_path}/#{appcast.feed_filename}", 'w') do |f|
- xml_string = ''
- doc = REXML::Formatters::Pretty.new
- doc.write(appcast_xml, xml_string)
- f << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
- f << xml_string
- f << "\n"
- end
- if appcast_file
- App.info "Create", "./#{sparkle_release_path}/#{appcast.feed_filename}"
- else
- App.info "Fail", "./#{sparkle_release_path}/#{appcast.feed_filename} not created"
- end
- end
+ # --download-url-prefix <url> A URL that will be used as prefix for the URL from
+ # where updates will be downloaded.
+ args << "--download-url-prefix=#{appcast.package_base_url}" if appcast.package_base_url.present?
- def appcast_xml
- rss = REXML::Element.new 'rss'
- rss.attributes['xmlns:atom'] = "http://www.w3.org/2005/Atom"
- rss.attributes['xmlns:sparkle'] = "http://www.andymatuschak.org/xml-namespaces/sparkle"
- rss.attributes['xmlns:version'] = "2.0"
- rss.attributes['xmlns:dc'] = "http://purl.org/dc/elements/1.1/"
- channel = rss.add_element 'channel'
- channel.add_element('title').text = @config.name
- channel.add_element('description').text = "#{@config.name} updates"
- channel.add_element('link').text = @config.info_plist["SUFeedURL"]
- channel.add_element('language').text = 'en'
- channel.add_element('pubDate').text = Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
- atom_link = channel.add_element('atom:link')
- atom_link.attributes['href'] = @config.info_plist["SUFeedURL"]
- atom_link.attributes['rel'] = 'self'
- atom_link.attributes['type'] = "application/rss+xml"
- item = channel.add_element 'item'
- item.add_element('title').text = "#{@config.name} #{@config.short_version}"
- item.add_element('pubDate').text = Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
- guid = item.add_element('guid')
- guid.text = "#{@config.name}-#{@config.short_version}"
- guid.attributes['isPermaLink'] = false
- item.add_element('sparkle:releaseNotesLink').text = "#{appcast.notes_url}"
- enclosure = item.add_element('enclosure')
- enclosure.attributes['url'] = "#{appcast.package_url}"
- enclosure.attributes['length'] = "#{@package_size}"
- enclosure.attributes['type'] = "application/octet-stream"
- enclosure.attributes['sparkle:version'] = @config.version
- enclosure.attributes['sparkle:shortVersionString'] = @config.short_version
- enclosure.attributes['sparkle:dsaSignature'] = @package_signature
- rss
- end
+ # --release-notes-url-prefix <url> A URL that will be used as prefix for constructing
+ # URLs for release notes.
+ args << "--release-notes-url-prefix=#{appcast.notes_base_url}" if appcast.notes_base_url.present?
- def release_notes_template_path
- sparkle_config_path + "release_notes.template.erb"
- end
+ # --link <link> A URL to the application's website which Sparkle may
+ # use for directing users to if they cannot download a
+ # new update from within the application. This will be
+ # used for new generated update items. By default, no
+ # product link is used.
- def release_notes_content_path
- sparkle_config_path + "release_notes.content.html"
- end
+ # --versions <versions> An optional comma delimited list of application
+ # versions (specified by CFBundleVersion) to generate
+ # new update items for. By default, new update items
+ # are inferred from the available archives and are only
+ # generated if they are in the latest 5 updates in the
+ # appcast.
- def release_notes_path
- sparkle_release_path + appcast.notes_filename.to_s
- end
+ # --maximum-deltas <maximum-deltas>
+ # The maximum number of delta items to create for the
+ # latest update for each minimum required operating
+ # system. (default: 5)
- 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}"
+ # --channel <channel-name>
+ # The Sparkle channel name that will be used for
+ # generating new updates. By default, no channel is
+ # used. Old applications need to be using Sparkle 2 to
+ # use this feature.
+
+ # --major-version <major-version>
+ # The last major or minimum autoupdate sparkle:version
+ # that will be used for generating new updates. By
+ # default, no last major version is used.
+
+ # --phased-rollout-interval <phased-rollout-interval>
+ # The phased rollout interval in seconds that will be
+ # used for generating new updates. By default, no
+ # phased rollout interval is used.
+
+ # --critical-update-version <critical-update-version>
+ # The last critical update sparkle:version that will be
+ # used for generating new updates. An empty string
+ # argument will treat this update as critical coming
+ # from any application version. By default, no last
+ # critical update version is used. Old applications
+ # need to be using Sparkle 2 to use this feature.
+
+ # --informational-update-versions <informational-update-versions>
+ # A comma delimited list of application
+ # sparkle:version's that will see newly generated
+ # updates as being informational only. An empty string
+ # argument will treat this update as informational
+ # coming from any application version. By default,
+ # updates are not informational only. --link must also
+ # be provided. Old applications need to be using
+ # Sparkle 2 to use this feature.
+
+ # -o <output-path> Path to filename for the generated appcast (allowed
+ # when only one will be created).
+
+ # -f <private-dsa-key-file> Path to the private DSA key file. Only use this
+ # option for transitioning to EdDSA from older updates.
+ # Note: only for supporting a legacy app that used DSA keys. Check if the
+ # default DSA key exists in `sparkle/config/dsa_priv.pem` and if it does,
+ # add it to the command.
+ if File.exist?(legacy_private_key_path)
+ App.info 'Sparkle', "Also signing with legacy DSA key at #{legacy_private_key_path}"
+ args << "-f=#{legacy_private_key_path}"
+ end
+
+ args << "-o=#{appcast_filename}" if appcast_filename.present?
+
+ 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_EdDSA_key}".indent(11)
end
- end
+ # rubocop:enable Metrics/CyclomaticComplexity
- def release_notes_html
- release_notes_content
- end
+ def generate_appcast_help
+ generate_appcast_app = "#{vendored_sparkle_path}/bin/generate_appcast"
+ results, _status = Open3.capture2e(generate_appcast_app, '--help')
+ puts results
+ end
+ 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)
- class Appcast
- attr_accessor :base_url,
- :feed_base_url,
- :feed_filename,
- :notes_base_url,
- :notes_filename,
- :package_base_url,
- :package_filename,
- :archive_folder
+ create_release_folder
- def initialize
- @feed_base_url = nil
- @feed_filename = 'releases.xml'
- @notes_base_url = nil
- @notes_filename = 'release_notes.html'
- @package_base_url = nil
- @package_filename = nil
- @base_url = nil
- @archive_folder = nil
+ 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
- def feed_url
- "#{feed_base_url || base_url}/#{feed_filename}"
+ def release_notes_template_path
+ sparkle_config_path.join('release_notes.template.erb')
end
- def notes_url
- "#{notes_base_url || base_url}/#{notes_filename}"
+ def release_notes_content_path
+ sparkle_config_path.join('release_notes.content.html')
end
- def package_url
- "#{package_base_url || base_url}/#{package_filename}"
+ def release_notes_path
+ sparkle_release_path + (appcast.notes_filename || "#{app_name}.#{@config.short_version}.html")
end
- end
+ 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
+ def release_notes_html
+ release_notes_content
+ end
+
+ class Appcast
+ attr_accessor :base_url,
+ :feed_base_url,
+ :feed_filename,
+ :notes_filename,
+ :package_filename,
+ :archive_folder,
+ :use_exported_private_key
+ attr_writer :notes_base_url,
+ :package_base_url
+
+ def initialize
+ @feed_base_url = nil
+ @feed_filename = 'releases.xml'
+ @notes_base_url = nil
+ @notes_filename = nil
+ @package_base_url = nil
+ @package_filename = nil
+ @base_url = nil
+ @archive_folder = nil
+ @use_exported_private_key = false
+ end
+
+ def feed_url
+ "#{feed_base_url || base_url}#{feed_filename}"
+ end
+
+ def notes_base_url
+ @notes_base_url || base_url
+ end
+
+ def package_base_url
+ @package_base_url || base_url
+ end
+ end
+ end
end
end