lib/motion/project/appcast.rb in motion-sparkle-sandbox-2.1.0 vs lib/motion/project/appcast.rb in motion-sparkle-sandbox-2.1.1
- old
+ new
@@ -1,200 +1,310 @@
# frozen_string_literal: true
+# rubocop:disable Metrics/ClassLength
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)
+ class Appcast
+ PARAMS = %i[
+ package_base_url
+ package_filename
+ notes_base_url
+ notes_filename
+ use_exported_private_key
+ base_url
+ releases_folder
+ feed_base_url
+ feed_filename
+ ].freeze
- args = []
+ CLI_OPTIONS = {
+ account: '--account',
+ private_eddsa_key: '-s',
+ download_url_prefix: '--download-url-prefix',
+ release_notes_url_prefix: '--release-notes-url-prefix',
+ full_release_notes_url: '--full-release-notes-url',
+ link: '--link',
+ versions: '--versions',
+ maximum_deltas: '--maximum-deltas',
+ delta_compression: '--delta-compression',
+ delta_compression_level: '--delta-compression-level',
+ channel: '--channel',
+ major_version: '--major-version',
+ ignore_skipped_upgrades_below_version: '--ignore-skipped-upgrades-below-version',
+ phased_rollout_interval: '--phased-rollout-interval',
+ critical_update_version: '--critical-update-version',
+ informational_update_versions: '--informational-update-versions',
+ output_path: '-o'
+ }.freeze
- FileUtils.mkdir_p(path) unless File.exist?(path)
+ attr_accessor :base_url,
+ :feed_base_url,
+ :feed_filename,
+ :notes_filename,
+ :package_filename,
+ :releases_folder,
+ :use_exported_private_key,
+ :cli_options
+ attr_writer :notes_base_url,
+ :package_base_url
- App.info('Sparkle', "Generating appcast using `#{generate_appcast_app}`")
- puts "from files in `#{path}`...".indent(11)
+ def initialize(sparkle_object)
+ @sparkle = sparkle_object
+ @cli_options = {
+ account: 'ed25519' # Sparkle's default account
+ }
- 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}"
+ @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
+ @releases_folder = nil
+ @use_exported_private_key = false
end
+ def process_option(key, value)
+ if CLI_OPTIONS.keys.include?(key)
+ cli_options[key] = value
+ elsif PARAMS.include?(key)
+ send("#{key}=", value)
+ else
+ return false
+ end
+
+ true
+ 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
+
+ def prepare_args
+ args = []
+
+ account(args)
+ private_eddsa_key(args)
+ download_url_prefix(args)
+ release_notes_url_prefix(args)
+ full_release_notes_url(args)
+ link(args)
+ versions(args)
+ maximum_deltas(args)
+ delta_compression(args)
+ delta_compression_level(args)
+ channel(args)
+ major_version(args)
+ ignore_skipped_upgrades_below_version(args)
+ phased_rollout_interval(args)
+ critical_update_version(args)
+ informational_update_versions(args)
+ output_path(args)
+
+ args
+ end
+
+ private
+
+ # --account <account> The account name in your keychain associated with
+ # your private EdDSA (ed25519) key to use for signing
+ # new updates. (default: ed25519)
+ def account(args)
+ return unless cli_options[:account].present?
+
+ args << "--account=#{cli_options[:account]}"
+ end
+
+ # -s <private-EdDSA-key> The private EdDSA string (128 characters). If not
+ # specified, the private EdDSA key will be read from
+ # the Keychain instead.
+ def private_eddsa_key(args)
+ if cli_options[:private_eddsa_key].present?
+ args << "-s=#{cli_options[:private_eddsa_key]}"
+ elsif use_exported_private_key && File.exist?(private_key_path)
+ private_key = File.read(private_key_path)
+ args << "-s=#{private_key}"
+ 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 download_url_prefix(args)
+ if cli_options[:download_url_prefix].present?
+ args << "--download-url-prefix=#{cli_options[:download_url_prefix]}"
+ elsif package_base_url.present?
+ args << "--download-url-prefix=#{package_base_url}"
+ end
+ 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_url_prefix(args)
+ if cli_options[:release_notes_url_prefix].present?
+ args << "--release-notes-url-prefix=#{cli_options[:release_notes_url_prefix]}"
+ elsif notes_base_url.present?
+ args << "--release-notes-url-prefix=#{notes_base_url}"
+ end
+ end
+ # --full-release-notes-url <url>
+ # A URL that will be used for the full release notes.
+ def full_release_notes_url(args)
+ return unless cli_options[:full_release_notes_url].present?
+
+ args << "--full-release-notes-url=#{cli_options[:full_release_notes_url]}"
+ 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 link(args)
+ return unless cli_options[:link].present?
+ args << "--link=#{cli_options[:link]}"
+ 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 versions(args)
+ return unless cli_options[:versions].present?
+ args << "--versions=#{cli_options[:versions]}"
+ 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 maximum_deltas(args)
+ return unless cli_options[:maximum_deltas].present?
+ args << "--maximum-deltas=#{cli_options[:maximum_deltas]}"
+ end
+
+ # --delta-compression <delta-compression>
+ # The compression method to use for generating delta
+ # updates. Supported methods for version 3 delta files
+ # are 'lzma', 'bzip2', 'zlib', 'lzfse', 'lz4', 'none',
+ # and 'default'. Note that version 2 delta files only
+ # support 'bzip2', 'none', and 'default' so other
+ # methods will be ignored if version 2 files are being
+ # generated. The 'default' compression for version 3
+ # delta files is currently lzma. (default: default)
+ def delta_compression(args)
+ return unless cli_options[:delta_compression].present?
+
+ args << "--delta-compression=#{cli_options[:delta_compression]}"
+ end
+
+ # --delta-compression-level <delta-compression-level>
+ # The compression level to use for generating delta
+ # updates. This only applies if the compression method
+ # used is bzip2 which accepts values from 1 - 9. A
+ # special value of 0 will use the default compression
+ # level. (default: 0)
+ def delta_compression_level(args)
+ return unless cli_options[:delta_compression_level].present?
+
+ args << "--delta-compression-level=#{cli_options[:delta_compression_level]}"
+ end
+
# --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.
+ def channel(args)
+ return unless cli_options[:channel].present?
+ args << "--channel=#{cli_options[:channel]}"
+ end
+
# --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.
+ def major_version(args)
+ return unless cli_options[:major_version].present?
+ args << "--major-version=#{cli_options[:major_version]}"
+ end
+
+ # --ignore-skipped-upgrades-below-version <below-version>
+ # Ignore skipped major upgrades below this specified
+ # version. Only applicable for major upgrades.
+ def ignore_skipped_upgrades_below_version(args)
+ return unless cli_options[:ignore_skipped_upgrades_below_version].present?
+
+ args << "--ignore-skipped-upgrades-below-version=#{cli_options[:ignore_skipped_upgrades_below_version]}"
+ end
+
# --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.
+ def phased_rollout_interval(args)
+ return unless cli_options[:phased_rollout_interval].present?
+ args << "--phased-rollout-interval=#{cli_options[:phased_rollout_interval]}"
+ end
+
# --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.
+ def critical_update_version(args)
+ return unless cli_options[:critical_update_version].present?
+ args << "--critical-update-version=#{cli_options[:critical_update_version]}"
+ end
+
# --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.
+ # coming from any application version. Prefix a version
+ # string with '<' to indicate (eg "<2.5") to indicate
+ # older versions than the one specified should treat
+ # the update as informational only. By default, updates
+ # are not informational only. --link must also be
+ # provided. Old applications need to be using Sparkle 2
+ # to use this feature, and 2.1 or later to use the '<'
+ # upper bound feature.
+ def informational_update_versions(args)
+ return unless cli_options[:informational_update_versions].present?
+ args << "--informational-update-versions=#{cli_options[:informational_update_versions]}"
+ end
+
# -o <output-path> Path to filename for the generated appcast (allowed
# when only one will be created).
+ def output_path(args)
+ return unless cli_options[:output_path].present?
- # -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}"
+ args << "-o=#{cli_options[:output_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
- # rubocop:enable Metrics/CyclomaticComplexity
-
- 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)
-
- 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
-
- def release_notes_template_path
- sparkle_config_path.join('release_notes.template.erb')
- end
-
- def release_notes_content_path
- sparkle_config_path.join('release_notes.content.html')
- end
-
- def release_notes_path
- sparkle_release_path + (appcast.notes_filename || "#{app_name}.#{@config.short_version}.html")
- 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
+# rubocop:enable Metrics/ClassLength