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