autosparkle 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d68b72cb179b558388027771604619562da6a0293a8901b3aadf9c39fbd39df7
4
+ data.tar.gz: eaa1e667baba2cd385a63de1d5102ca950b37ecc154b5c8256e6087947d8e973
5
+ SHA512:
6
+ metadata.gz: 787e99f1fd204d1a9234d22109fc92a2572d09381c8277ffd83e85423ee9fbc286547c700e449876ac6ea3a2b7bf4912901856a5ba4fbd5875e9431ff2e3c540
7
+ data.tar.gz: 23c2358c76555744a099b67441e032b422f6e5a068f62edda8fae42b74531296e053fe4302bc883ccfa3770911e038238054654d23fbfab7c09482d2aeb4b73c
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # autosparkle
2
+
3
+ ![autosparkle main image](resources/readme-main-image.jpg)
4
+
5
+ autosparkle simplifies the process of **archiving**, **exporting**, **signing**, **packaging**, **notarizing**, and **uploading** your new version of the macOS app outside the App Store.
6
+
7
+ With autosparkle, you no longer have to worry about these steps anymore.
8
+
9
+ ## Table of Contents
10
+ - [Introduction](#introduction)
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Environment](#environment)
14
+ - [Storage](#storage)
15
+ - [Future Enhancements](#future-enhancements)
16
+ - [Contributing](#contributing)
17
+ - [License](#license)
18
+
19
+ ## Introduction
20
+
21
+ Pushing a new version of your macOS application outside the App Store can be time-consuming. This is because there are several steps required by Apple, in addition to the ones related to Sparkle.
22
+
23
+ autosparkle is a Ruby command line tool that automates the delivery of your macOS applications outside the App Store. It is implemented around the [Sparkle](https://github.com/sparkle-project/Sparkle) framework.
24
+
25
+ > **Note:**
26
+ >
27
+ > *This tool is designed for use in a Continuous Delivery (CD) workflow. Although it is possible to run it locally, doing so is not recommended due to the creation of a custom keychain and the mounting of a new DMG. However, if you are comfortable with these steps, you can run it locally without any issues.*
28
+
29
+ ## Features
30
+ - Archive and export your signed application using the provided Developer Application ID certificate.
31
+ - Package your app into a signed DMG.
32
+ - Customize your DMG appearance by adding a custom background image and a symbolic link to drag the app to the Applications folder.
33
+ - Notarize and staple your DMG.
34
+ - Automatically sign the Sparkle framework.
35
+ - Automatically handle your app versioning based on the specified bump method and the versions uploaded to the storage.
36
+ - Generate or update the appcast.xml file used by Sparkle.
37
+ - Upload the appcast.xml and your new version to the specified storage.
38
+ - Use environment files, making it easy to create a specific one for use in CD *(e.g., env.autosparkle.production)*.
39
+
40
+ ## Installation
41
+
42
+ #### Using Ruby Gems:
43
+ 1. Run the following command:
44
+ ```bash
45
+ gem install autosparkle
46
+ ```
47
+ 2. (optional) Add autosparkle to your shell configuration file (e.g. ~/.bashrc or ~/.zshrc) by appending the following line:
48
+ ```bash
49
+ export PATH="<<YOUR_GEMS_DIR>>/autosparkle-x.x.x/bin:$PATH"
50
+ ```
51
+ Make sure to replace `<<YOUR_GEMS_DIR>>/autosparkle-x.x.x` with the actual path to the autosparkle gem on your system.
52
+
53
+ 3. (optional) Run the following command: `source ~/.bashrc` or `source ~/.zshrc`.
54
+
55
+
56
+ #### Manual install:
57
+
58
+ 1. Clone the repository:
59
+ ```bash
60
+ git clone https://github.com/hadiidbouk/autosparkle.git
61
+ ```
62
+ 2. Call the `autosparkle.rb` script from the repository, you need to specify your Xcode project path and your environment file. Here's how you can do it:
63
+ ```bash
64
+ ruby autosparkle.rb --project-path <<YOUR XCODE PROJECT>> --env <<YOUR ENV NAME>>
65
+ ```
66
+
67
+ You can also pass your workspace path using `--workspace-path` and specify the environment path using `--env-file`.
68
+
69
+
70
+ ## Environment
71
+ autosparkle utilizes the [dotenv](https://github.com/bkeepers/dotenv) gem to manage environment variables.
72
+
73
+ The purpose of using environment variable files is to facilitate the transition between the local environment on your personal machine and the production environment on your continuous delivery machine.
74
+
75
+ When you specify the environment name using the `--env` command, autosparkle will search for autosparkle environment files *(e.g., .env.autosparkle.local, .env.autosparkle.production)* in your project directory.
76
+
77
+ Remember to add your local environment file to the *.gitignore* file and remove any sensitive data from your production environment file. Instead, replace them with the environment variables provided by your continuous delivery system like this:
78
+ ```
79
+ APP_SEPECIFIC_PASSWORD=$APP_SPECIFIC_PASSWORD
80
+ ```
81
+
82
+ Below are the list of environment variables used by autosparkle:
83
+
84
+ | Variable | Description | Required | Default value |
85
+ |----------|-------------|----------|---------------|
86
+ | SCHEME | The macOS app scheme | Yes | |
87
+ | CONFIGURATION | The app configuration (Debug/Release..)| Yes | |
88
+ | APPLE_ID | The Apple ID that will be used in the notarize step | Yes | |
89
+ | APP_SPECIFIC_PASSWORD | The App specific password that will be used in the notarize step | Yes | |
90
+ | DEVELOPER_ID_APPLICATION_BASE64 | The Developer ID Application base64 certificate | Yes | |
91
+ | DEVELOPER_ID_APPLICATION_PASSWORD | The Developer ID Application base64 certificate password | Yes | |
92
+ | DMG_BACKGROUND_IMAGE | The DMG background image path that should exist in the project folder | Yes | |
93
+ | DMG_WINDOW_WIDTH | The DMG "Drag to Applications" window width | Yes | |
94
+ | DMG_WINDOW_HEIGHT | The DMG "Drag to Applications" window height | Yes | |
95
+ | DMG_ICON_SIZE | The icon size of the app and the Applications folder in the window | Yes | |
96
+ | SPARKLE_PRIVATE_KEY | The Sparkle private key that will be generated on your machine after running the `lib/autosparkle/sparkle/generate_keys`, you can export the private key into a file and then copy it into this variable. `lib/autosparkle/sparkle/generate_keys -x ~/Desktop/my_key.txt` | Yes | |
97
+ | SPARKLE_UPDATE_TITLE | The new version update title | Yes | |
98
+ | SPARKLE_RELEASE_NOTES | The HTML release notes, it shouldn't contains any body or header just simple html tags | Yes | |
99
+ | SPARKLE_BUMP_VERSION_METHOD | The version bump method for your marketing semantic version, `patch`, `minor`, `major` or `same`. | Yes | |
100
+ | WEBSITE_URL | The website URL that will be added to the appcast.xml | Yes | |
101
+ | STORAGE_TYPE | The storage type used to upload your app versions alongside the *appcast.yml* file, available values are: `aws-s3` | Yes | |
102
+ | AWS_S3_ACCESS_KEY | The AWS S3 access Key | Yes (if `aws-s3` STORAGE_TYPE is specified) | |
103
+ | AWS_S3_SECRET_ACCESS_KEY | The AWS S3 secret access key | Yes (if `aws-s3` STORAGE_TYPE is specified)| |
104
+ | AWS_S3_REGION | The AWS S3 region | Yes (if `aws-s3` STORAGE_TYPE is specified) | |
105
+ | AWS_S3_BUCKET_NAME | The AWS S3 bucket name | Yes (if `aws-s3` STORAGE_TYPE is specified) | |
106
+
107
+ > **Note:**
108
+ >
109
+ > *Remember to add the SUFeedURL and the **SUPublicEDKey** to your app's Info.plist.*
110
+ > **SUFeedURL** is the URL of your appcast.xml, which should be publicly accessible online.
111
+ > **SUPublicEDKey** will be displayed in your terminal after running the `bin/generate_keys` command.
112
+
113
+ You can refer to the [SparkleTestApp](https://github.com/hadiidbouk/sparkle-test-app) sample project for more information.
114
+
115
+ ## Storage
116
+ In order to support multiple versions of your macOS application, autosparkle requires access to an online storage for uploading and reading the *appcast.xml* file and the application versions.
117
+
118
+ The currently supported storage options are:
119
+
120
+ - AWS S3
121
+
122
+ ## Future Enhancements
123
+ Here are some future enhancements planned for autosparkle:
124
+
125
+ - Support binary delta updates.
126
+ - Support for additional storage options.
127
+ - Introduce the ability to package the app without distributing it.
128
+ - Support for the `pkg` format.
129
+ - Distributing autosparkle using Homebrew.
130
+
131
+ Stay tuned for these exciting updates!
132
+
133
+
134
+
135
+ ## Contributing
136
+ Contributions to autosparkle are welcome! If you would like to contribute, please follow these guidelines:
137
+ - Fork the repository.
138
+ - Create a new branch for your feature or bug fix.
139
+ - Make your changes and commit them with descriptive messages.
140
+ - Push your changes to your forked repository.
141
+ - Submit a pull request to the main repository on the `develop` branch.
142
+
143
+ ## License
144
+ autosparkle is licensed under the [MIT License](LICENSE).
data/bin/autosparkle ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/autosparkle/main'
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require_relative 'helpers/build_directory_helpers'
5
+ require_relative 'helpers/commands_helpers'
6
+ require_relative 'helpers/puts_helpers'
7
+ require_relative 'helpers/constants'
8
+ require_relative 'helpers/appcast_helpers'
9
+ require_relative 'environment/environment'
10
+
11
+ # Distribution module to sign the update and upload it to the server
12
+ module Distribution
13
+ def self.upload_update(pkg_path)
14
+ puts_title 'Uploading the update to the server'
15
+ ed_signature_fragment = sign_update(pkg_path)
16
+
17
+ appcast_xml = AppcastXML.generate_appcast_xml(ed_signature_fragment, Env.storage.deployed_appcast_xml)
18
+ upload_update_to_server(pkg_path, appcast_xml)
19
+
20
+ app_display_name = Env.variables.app_display_name
21
+ version = Env.variables.marketing_version
22
+ build_version = Env.variables.current_project_version
23
+ puts "#{app_display_name} version #{version} (#{build_version}) has been uploaded successfully. ✅ 🚀".green
24
+ end
25
+
26
+ def self.sign_update(pkg_path)
27
+ puts_if_verbose 'Signing the update...'
28
+ sign_update_path = File.join(__dir__, 'sparkle', 'sign_update')
29
+ sign_command = "echo \"#{Env.variables.sparkle_private_key}\" | "
30
+ sign_command += "#{sign_update_path} \"#{pkg_path}\" --ed-key-file -"
31
+ execute_command(sign_command, contains_sensitive_data: true)
32
+ end
33
+
34
+ def self.upload_update_to_server(pkg_path, appcast_xml)
35
+ puts_if_verbose 'Uploading the update to the server...'
36
+ appcast_file = BuildDirectory.new_file('appcast.xml')
37
+ appcast_file.write(appcast_xml)
38
+ appcast_file.close
39
+
40
+ Env.storage.upload(pkg_path, appcast_file.path)
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv'
4
+ require 'nokogiri'
5
+ require_relative '../helpers/appcast_helpers'
6
+ require_relative '../helpers/puts_helpers'
7
+ require_relative '../storages/aws_s3_storage'
8
+ require_relative 'variables/default_environment_variables'
9
+
10
+ # Environment module to load the environment variables
11
+ # It contains the app state
12
+ module Env
13
+ @variables = nil
14
+ @storage = nil
15
+ @verbose_enabled = false
16
+
17
+ class << self
18
+ attr_accessor :variables, :storage, :verbose_enabled
19
+
20
+ def initialize(options)
21
+ project_path = options[:project_path]
22
+ workspace_path = options[:workspace_path]
23
+ project_directory_path = File.dirname(workspace_path || project_path)
24
+
25
+ load_environment(project_directory_path, options)
26
+
27
+ @variables = DefaultEnvironmentVariables.new(project_directory_path)
28
+ @storage = retrieve_storage
29
+
30
+ retrieve_variables_from_xcode(project_path, workspace_path)
31
+ set_up_app_versions(project_path, workspace_path)
32
+
33
+ puts_if_verbose "Running the script with the #{options[:env]} environment...\n"
34
+ end
35
+ end
36
+
37
+ def self.load_environment(project_directory_path, options)
38
+ env_file_path = options[:env_file] || File.join(project_directory_path, ".env.autosparkle.#{options[:env]}")
39
+ raise "#{env_file_path} does not exist." unless File.exist?(env_file_path)
40
+
41
+ Dotenv.load(env_file_path)
42
+ end
43
+
44
+ def self.retrieve_storage
45
+ storage_type = ENV.fetch('STORAGE_TYPE', nil)
46
+ raise 'Storage type is not defined in the environment' if storage_type.nil? || storage_type.empty?
47
+
48
+ case storage_type
49
+ when 'aws-s3'
50
+ AwsS3Storage.new
51
+ else
52
+ raise "Storage type #{storage_type} is not supported"
53
+ end
54
+ end
55
+
56
+ def self.retrieve_variables_from_xcode(project_path, workspace_path)
57
+ puts_if_verbose 'Fetching the minimum macOS version from the Xcode project...'
58
+ ENV['MINIMUM_MACOS_VERSION'] = Xcodeproj.get_minimum_deployment_macos_version(project_path, workspace_path)
59
+
60
+ puts_if_verbose 'Fetching the app display name from the Xcode project...'
61
+ ENV['APP_DISPLAY_NAME'] = Xcodeproj.get_app_display_name(project_path, workspace_path)
62
+ end
63
+
64
+ def self.set_up_app_versions(project_path, workspace_path)
65
+ marketing_version, current_project_version = AppcastXML.retreive_versions(@storage.deployed_appcast_xml)
66
+
67
+ ENV['MARKETING_VERSION'] = marketing_version
68
+ ENV['CURRENT_PROJECT_VERSION'] = current_project_version
69
+
70
+ puts_if_verbose 'Updating the project versions from the environment variables...'
71
+ Xcodeproj.update_project_version(project_path, workspace_path)
72
+ end
73
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_environment_variables'
4
+
5
+ # A class to load the AWS S3 environment variables
6
+ class AwsS3EnvironmentVariables < BaseEnvironmentVariables
7
+ def initialize
8
+ super({
9
+ access_key: 'AWS_S3_ACCESS_KEY',
10
+ secret_access_key: 'AWS_S3_SECRET_ACCESS_KEY',
11
+ region: 'AWS_S3_REGION',
12
+ bucket_name: 'AWS_S3_BUCKET_NAME'
13
+ })
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../helpers/puts_helpers'
4
+
5
+ # The base envrionment variables class
6
+ class BaseEnvironmentVariables
7
+ def initialize(variables)
8
+ @variables = variables
9
+ end
10
+
11
+ def method_missing(method_name, *arguments, &block)
12
+ key = method_name.to_sym
13
+ if @variables.key?(key)
14
+ value = ENV.fetch(@variables[key], nil)
15
+ raise "#{@variables[key]} is not defined in the environment variables" if value.nil? || value.empty?
16
+
17
+ value
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def respond_to_missing?(method_name, include_private = false)
24
+ @variables.key?(method_name.to_sym) || super
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../helpers/puts_helpers'
4
+ require_relative '../../helpers/xcodeproj_helpers'
5
+ require_relative 'base_environment_variables'
6
+
7
+ # A class to load the default environment variables
8
+ class DefaultEnvironmentVariables < BaseEnvironmentVariables
9
+ def initialize(project_directory_path)
10
+ super({
11
+ project_directory_path: 'PROJECT_DIRECTORY_PATH',
12
+ scheme: 'SCHEME',
13
+ configuration: 'CONFIGURATION',
14
+ marketing_version: 'MARKETING_VERSION',
15
+ current_project_version: 'CURRENT_PROJECT_VERSION',
16
+ minimum_macos_version: 'MINIMUM_MACOS_VERSION',
17
+ app_display_name: 'APP_DISPLAY_NAME',
18
+ apple_id: 'APPLE_ID',
19
+ app_specific_password: 'APP_SPECIFIC_PASSWORD',
20
+ developer_id_application_password: 'DEVELOPER_ID_APPLICATION_PASSWORD',
21
+ developer_id_application_base64: 'DEVELOPER_ID_APPLICATION_BASE64',
22
+ sparkle_private_key: 'SPARKLE_PRIVATE_KEY',
23
+ sparkle_update_title: 'SPARKLE_UPDATE_TITLE',
24
+ sparkle_release_notes: 'SPARKLE_RELEASE_NOTES',
25
+ sparkle_bump_version_method: 'SPARKLE_BUMP_VERSION_METHOD',
26
+ website_url: 'WEBSITE_URL',
27
+ dmg_background_image: 'DMG_BACKGROUND_IMAGE',
28
+ dmg_icon_size: 'DMG_ICON_SIZE',
29
+ dmg_window_width: 'DMG_WINDOW_WIDTH',
30
+ dmg_window_height: 'DMG_WINDOW_HEIGHT'
31
+ })
32
+
33
+ ENV['PROJECT_DIRECTORY_PATH'] = project_directory_path
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require_relative '../environment/environment'
5
+ require_relative 'puts_helpers'
6
+ require_relative 'version_helpers'
7
+
8
+ # AppcastXML module to generate the appcast XML file and items
9
+ module AppcastXML
10
+ def self.generate_appcast_xml(ed_signature_fragment, deployed_appcast_xml)
11
+ if deployed_appcast_xml
12
+ append_item_to_existing_appcast(ed_signature_fragment, deployed_appcast_xml)
13
+ else
14
+ puts_if_verbose 'Creating a new appcast file...'
15
+ <<~XML
16
+ <?xml version="1.0" encoding="utf-8"?>
17
+ <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
18
+ <channel>
19
+ <title>Changelog</title>
20
+ <description>Most recent changes with links to updates.</description>
21
+ <language>en</language>
22
+ #{generate_appcast_item(ed_signature_fragment)}
23
+ </channel>
24
+ </rss>
25
+ XML
26
+ end
27
+ end
28
+
29
+ def self.append_item_to_existing_appcast(ed_signature_fragment, deployed_appcast_xml)
30
+ puts_if_verbose 'Appending the new item to the existing appcast file...'
31
+
32
+ doc = Nokogiri::XML(deployed_appcast_xml)
33
+
34
+ new_item_xml = generate_appcast_item(ed_signature_fragment)
35
+ new_item_doc = Nokogiri::XML(new_item_xml)
36
+ new_item = new_item_doc.root
37
+
38
+ channel = doc.at_xpath('//channel')
39
+ channel.add_child(new_item)
40
+
41
+ doc.to_xml
42
+ end
43
+
44
+ def self.generate_appcast_item(ed_signature_fragment)
45
+ date = Time.now.strftime('%a %b %d %H:%M:%S %Z %Y')
46
+ pkg_url = "#{Env.variables.marketing_version}/#{Env.variables.app_display_name}.dmg"
47
+ <<~XML
48
+ <item>
49
+ <title>#{Env.variables.sparkle_update_title}</title>
50
+ <link>#{Env.variables.website_url}</link>
51
+ <sparkle:version>#{Env.variables.current_project_version}</sparkle:version>
52
+ <sparkle:shortVersionString>#{Env.variables.marketing_version}</sparkle:shortVersionString>
53
+ <description>
54
+ <![CDATA[
55
+ #{Env.variables.sparkle_release_notes}
56
+ ]]>
57
+ </description>
58
+ <pubDate>#{date}</pubDate>
59
+ <enclosure url="#{pkg_url}" type="application/octet-stream" #{ed_signature_fragment} />
60
+ <sparkle:minimumSystemVersion>#{Env.variables.minimum_macos_version}</sparkle:minimumSystemVersion>
61
+ </item>
62
+ XML
63
+ end
64
+
65
+ def self.retreive_versions(deployed_appcast_xml)
66
+ return ['1.0.0', '1'] unless deployed_appcast_xml
67
+
68
+ doc = Nokogiri::XML(deployed_appcast_xml)
69
+ [marketing_version(doc), current_project_version(doc)]
70
+ end
71
+
72
+ def self.marketing_version(doc)
73
+ method_name = Env.variables.sparkle_bump_version_method
74
+ raise "Unsupported bump method name '#{method_name}'" unless %w[minor patch major same].include?(method_name)
75
+
76
+ # bump the marketing version from @variables.sparkle_bump_version_method
77
+ short_version_strings = doc.xpath('//item/sparkle:shortVersionString', 'sparkle' => 'http://www.andymatuschak.org/xml-namespaces/sparkle')
78
+ latest_semantic_version = short_version_strings.map { |s| Gem::Version.new(s.text) }.max
79
+ method_name == 'same' ? latest_semantic_version.to_s : Version.bump(latest_semantic_version, method_name)
80
+ end
81
+
82
+ def self.current_project_version(doc)
83
+ # find the latest versions in the item tag for sparkle:version child
84
+ versions = doc.xpath('//item/sparkle:version', 'sparkle' => 'http://www.andymatuschak.org/xml-namespaces/sparkle')
85
+ .map { |s| s.text.to_i }
86
+ latest_version = versions.max + 1
87
+ latest_version.to_s
88
+ end
89
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'constants'
5
+ require_relative 'puts_helpers'
6
+
7
+ # This module is used to create a new path inside the build directory
8
+ module BuildDirectory
9
+ @build_directory_path = File.expand_path(Constants::AUTOSPARKLE_BUILD_DIRECTORY_PATH)
10
+
11
+ def self.build_directory_path
12
+ @build_directory_path
13
+ end
14
+
15
+ def self.create_build_directory
16
+ if File.directory?(build_directory_path)
17
+ puts_if_verbose 'Cleaning up the build directory...'
18
+ FileUtils.rm_rf(build_directory_path)
19
+ end
20
+
21
+ puts_if_verbose "Creating the build directory at #{build_directory_path} ..."
22
+ FileUtils.mkdir_p(build_directory_path)
23
+ end
24
+
25
+ def self.new_path(name)
26
+ "#{build_directory_path}/#{name}"
27
+ end
28
+
29
+ def self.new_file(name)
30
+ File.open(new_path(name), 'w')
31
+ end
32
+
33
+ def self.new_directory(name)
34
+ path = new_path(name)
35
+ FileUtils.mkdir_p(path)
36
+ path
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require 'english'
5
+ require_relative '../environment/environment'
6
+
7
+ #
8
+ # This method executes a command and returns the output
9
+ # It raises an error if the command fails
10
+ #
11
+ def execute_command(command, contains_sensitive_data: false)
12
+ # if the command has --password, --secret then replace it with *****
13
+ presented_command = command.gsub(/(--password|--secret) \S+/, '\1 *****')
14
+
15
+ puts "\n#{presented_command}\n".cyan if Env.verbose_enabled && !contains_sensitive_data
16
+ stdout, stderr, status = Open3.capture3(command)
17
+
18
+ # if status is not success
19
+ unless status.success?
20
+ puts "\nCommand failed: #{command}\n".red
21
+ puts "\nError: #{stderr}\n".red
22
+ raise
23
+ end
24
+
25
+ puts "#{stdout}\n\n".magenta if Env.verbose_enabled && !contains_sensitive_data
26
+
27
+ stdout
28
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Constants module to store the constants used in the application
4
+ module Constants
5
+ NOTARIZE_KEYCHAIN_PROFILE = 'autosparkle.keychain.notarize.profile'
6
+ AUTOSPARKLE_BUILD_DIRECTORY_PATH = '~/Library/Developer/autosparkle/build'
7
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'build_directory_helpers'
5
+ require_relative 'puts_helpers'
6
+ require_relative '../environment/environment'
7
+
8
+ # This module is used to create a DMG file and set up the appearance
9
+ module DMG
10
+ def self.create(app_path)
11
+ volume_name = Env.variables.app_display_name
12
+ background_image_path = "#{Env.variables.project_directory_path}/#{Env.variables.dmg_background_image}"
13
+
14
+ dmg_path = create_blank_dmg(app_path, background_image_path, volume_name)
15
+ mount(dmg_path, volume_name)
16
+ copy_app_and_set_symbolic_link(app_path, volume_name)
17
+ copy_background_image(background_image_path, volume_name)
18
+ customize_dmg_appearence(volume_name)
19
+ unmount(volume_name)
20
+ create_read_only_dmg(dmg_path)
21
+ end
22
+
23
+ def self.create_blank_dmg(app_path, background_image_path, volume_name)
24
+ puts_if_verbose 'Creating a blank DMG...'
25
+ volume_size = volume_size(app_path, background_image_path)
26
+ uuid = `uuidgen`.strip
27
+ dmg_path = BuildDirectory.new_path("#{Env.variables.app_display_name}-#{uuid}.dmg")
28
+ execute_command("hdiutil create -size #{volume_size}m -fs HFS+ -volname '#{volume_name}' -ov '#{dmg_path}'")
29
+ dmg_path
30
+ end
31
+
32
+ def self.volume_size(app_path, background_image_path)
33
+ app_size_kb = `du -sk "#{app_path}"`.split("\t").first.to_i
34
+ background_size_kb = File.size(background_image_path) / 1024
35
+ buffer_size_kb = 20 * 1024
36
+ volume_size_kb = app_size_kb + background_size_kb + buffer_size_kb
37
+ (volume_size_kb / 1024.0).ceil
38
+ end
39
+
40
+ def self.mount(dmg_path, volume_name)
41
+ puts_if_verbose 'Mounting the DMG...'
42
+ execute_command("hdiutil attach '#{dmg_path}' -mountpoint '/Volumes/#{volume_name}'")
43
+ end
44
+
45
+ def self.copy_app_and_set_symbolic_link(app_path, volume_name)
46
+ puts_if_verbose 'Copying the app to the DMG and creating a symbolic link to the Applications folder...'
47
+ FileUtils.cp_r(app_path, "/Volumes/#{volume_name}")
48
+ execute_command("ln -s /Applications /Volumes/#{volume_name}/Applications")
49
+ end
50
+
51
+ def self.copy_background_image(background_image_path, volume_name)
52
+ puts_if_verbose 'Copying the background image to the DMG...'
53
+ FileUtils.mkdir_p("/Volumes/#{volume_name}/.background")
54
+ FileUtils.cp(background_image_path, "/Volumes/#{volume_name}/.background/")
55
+ end
56
+
57
+ def self.customize_dmg_appearence(volume_name)
58
+ puts_if_verbose 'Customizing the appearance of the DMG...'
59
+
60
+ app_x_position = Env.variables.dmg_window_width.to_i * 0.25
61
+ applications_x_position = Env.variables.dmg_window_width.to_i * 0.75
62
+ item_y_position = Env.variables.dmg_window_height.to_i / 2
63
+
64
+ apple_script = dmg_appearence_apple_script(volume_name, app_x_position, applications_x_position, item_y_position)
65
+ execute_command("osascript -e '#{apple_script}'")
66
+ end
67
+
68
+ def self.dmg_appearence_apple_script(volume_name, app_x_position, applications_x_position, item_y_position)
69
+ window_width = Env.variables.dmg_window_width.to_i
70
+ window_height = Env.variables.dmg_window_height.to_i
71
+ <<-APPLESCRIPT
72
+ tell application "Finder"
73
+ tell disk "#{volume_name}"
74
+ open
75
+ set current view of container window to icon view
76
+ set toolbar visible of container window to false
77
+ set statusbar visible of container window to false
78
+ set the bounds of container window to {0, 0, #{window_width}, #{window_height}}
79
+ set arrangement of icon view options of container window to not arranged
80
+ set icon size of icon view options of container window to #{Env.variables.dmg_icon_size}
81
+ set background picture of icon view options of container window to file ".background:#{Env.variables.dmg_background_image}"
82
+ set position of item "#{Env.variables.app_display_name}.app" of container window to {#{app_x_position}, #{item_y_position}}
83
+ set position of item "Applications" of container window to {#{applications_x_position}, #{item_y_position}}
84
+ close
85
+ open
86
+ update without registering applications
87
+ delay 5
88
+ end tell
89
+ end tell
90
+ APPLESCRIPT
91
+ end
92
+
93
+ def self.unmount(volume_name)
94
+ puts_if_verbose 'Unmounting the DMG...'
95
+ command = "hdiutil detach '/Volumes/#{volume_name}'"
96
+ begin
97
+ execute_command(command)
98
+ rescue StandardError
99
+ puts_if_verbose 'Retrying unmount after a brief wait...'
100
+ sleep 5
101
+ execute_command(command)
102
+ end
103
+ end
104
+
105
+ def self.create_read_only_dmg(dmg_path)
106
+ puts_if_verbose 'Converting the DMG to read-only...'
107
+ dmg_final_path = BuildDirectory.new_path("#{Env.variables.app_display_name}.dmg")
108
+ execute_command("hdiutil convert '#{dmg_path}' -format UDZO -o '#{dmg_final_path}'")
109
+ execute_command("rm -rf '#{dmg_path}'")
110
+ dmg_final_path
111
+ end
112
+ end
@@ -0,0 +1,107 @@
1
+ require 'securerandom'
2
+ require 'open3'
3
+ require 'base64'
4
+ require_relative 'build_directory_helpers'
5
+ require_relative 'constants'
6
+ require_relative 'commands_helpers'
7
+ require_relative 'puts_helpers'
8
+
9
+ #
10
+ # Execute the given block with a temporary keychain,
11
+ # The block will receive the application certificate name and team id as arguments.
12
+ # The temporary keychain will be deleted after the block has been executed.
13
+ #
14
+ def with_temporary_keychain
15
+ keychain_name = "temporary.autosparkle.keychain-#{SecureRandom.uuid}"
16
+ keychain_path = "~/Library/Keychains/#{keychain_name}-db"
17
+ password = SecureRandom.hex(16)
18
+
19
+ original_keychain_list = `security list-keychains`.strip.split("\n").map(&:strip)
20
+ default_keychain = execute_command('security default-keychain')
21
+ default_keychain_path = default_keychain.gsub(/"(.*)"/, '\1').strip
22
+
23
+ delete_temporary_keychain_if_needed(keychain_path)
24
+
25
+ begin
26
+ # Create a temporary keychain
27
+ create_temporary_keychain(keychain_path, password, original_keychain_list)
28
+ import_certificates_in_temporary_keychain(keychain_path)
29
+ execute_command("security set-key-partition-list -S apple-tool:,apple:,codesign: \\
30
+ -s -k \"#{password}\" #{keychain_path}")
31
+
32
+ # Fetch the certificate names and team ids from the temporary keychain
33
+ application_cert_name, application_team_id = fetch_application_certificate_info(keychain_path)
34
+ store_notarization_credentials(keychain_path, application_team_id)
35
+
36
+ keychain_info = {
37
+ keychain_path: keychain_path,
38
+ application_cert_name: application_cert_name,
39
+ application_team_id: application_team_id
40
+ }
41
+ yield(keychain_info) if block_given?
42
+ ensure
43
+ puts_if_verbose 'Ensuring cleanup of temporary keychain...'
44
+ delete_temporary_keychain_if_needed(keychain_path)
45
+
46
+ # Reset the keychain to the default
47
+ execute_command("security list-keychains -s #{original_keychain_list.join(' ')}")
48
+ execute_command("security default-keychain -s \"#{default_keychain_path}\"")
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def create_temporary_keychain(keychain_path, password, original_keychain_list)
55
+ execute_command("security create-keychain -p \"#{password}\" #{keychain_path}")
56
+ execute_command("security unlock-keychain -p \"#{password}\" #{keychain_path}")
57
+ execute_command("security list-keychains -d user -s #{(original_keychain_list + [keychain_path]).join(' ')}")
58
+ execute_command("security default-keychain -s #{keychain_path}")
59
+ end
60
+
61
+ def store_notarization_credentials(keychain_path, application_team_id)
62
+ command = "xcrun notarytool store-credentials #{Constants::NOTARIZE_KEYCHAIN_PROFILE} \\
63
+ --keychain #{keychain_path} \\
64
+ --apple-id #{Env.variables.apple_id} \\
65
+ --team-id #{application_team_id} \\
66
+ --password #{Env.variables.app_specific_password}"
67
+ execute_command(command)
68
+ end
69
+
70
+ def import_certificates_in_temporary_keychain(keychain_path)
71
+ developer_id_application_p12 = Base64.decode64(Env.variables.developer_id_application_base64 || '')
72
+
73
+ # Create temporary files for the .p12 certificates
74
+ application_cert_file = BuildDirectory.new_file('application_cert.p12')
75
+
76
+ # Write the decoded .p12 data to the temporary files
77
+ application_cert_file.write(developer_id_application_p12)
78
+ application_cert_file.close
79
+
80
+ # Import the certificates into the temporary keychain
81
+ import_file_to_keychain(keychain_path, application_cert_file.path,
82
+ Env.variables.developer_id_application_password)
83
+ end
84
+
85
+ def import_file_to_keychain(keychain_path, file_path, password)
86
+ command = "security import #{file_path} -k #{keychain_path} -P #{password}"
87
+ command += ' -T /usr/bin/codesign'
88
+ command += ' -T /usr/bin/security'
89
+ command += ' -T /usr/bin/productbuild'
90
+ command += ' -T /usr/bin/productsign'
91
+ execute_command(command)
92
+ end
93
+
94
+ def fetch_certificate_info(keychain_path, certificate_type)
95
+ command = "security find-certificate -c \"#{certificate_type}\" #{keychain_path} | grep \"labl\" | sed -E 's/^.*\"labl\"<blob>=\"(.*)\".*/\\1/'"
96
+ name = `#{command}`.strip
97
+ team_id = name[/\(([^)]+)\)$/, 1]
98
+ [name, team_id]
99
+ end
100
+
101
+ def fetch_application_certificate_info(keychain_path)
102
+ fetch_certificate_info(keychain_path, 'Developer ID Application')
103
+ end
104
+
105
+ def delete_temporary_keychain_if_needed(keychain_path)
106
+ execute_command("security delete-keychain #{keychain_path}") if File.exist?(File.expand_path(keychain_path.to_s))
107
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require_relative '../environment/environment'
5
+
6
+ def puts_if_verbose(message)
7
+ puts message if Env.verbose_enabled
8
+ end
9
+
10
+ def puts_error(message)
11
+ if message.is_a?(Array)
12
+ puts message.map(&:red).join("\n")
13
+ else
14
+ puts message.red
15
+ end
16
+ end
17
+
18
+ def puts_title(message)
19
+ puts "\n🔷 #{message} ...\n".yellow
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is used to bump the version of the app
4
+ # It receives the current version and the method to bump the version
5
+ # It returns the new version
6
+ module Version
7
+ def self.bump(current_version, method)
8
+ major, minor, patch = current_version.segments
9
+
10
+ case method
11
+ when 'major'
12
+ major += 1
13
+ # Reset minor and patch versions
14
+ minor = 0
15
+ patch = 0
16
+ when 'minor'
17
+ minor += 1
18
+ # Reset patch version
19
+ patch = 0
20
+ when 'patch'
21
+ patch += 1
22
+ else
23
+ raise ArgumentError, "Unknown bump method: #{method}"
24
+ end
25
+
26
+ "#{major}.#{minor}.#{patch}"
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ require 'colorize'
2
+ require 'xcodeproj'
3
+ require 'plist'
4
+ require 'optparse'
5
+ require_relative '../environment/environment'
6
+ require_relative 'puts_helpers'
7
+
8
+ # Xcodeproj module to handle the fetch and manipulation of Xcode project
9
+ module Xcodeproj
10
+ def self.get_app_display_name(project_path, workspace_path)
11
+ _, target = get_app_target(project_path, workspace_path)
12
+ build_settings = target.build_configurations.first.build_settings
13
+ display_name = build_settings['PRODUCT_NAME']
14
+ display_name = target.name if display_name == '$(TARGET_NAME)'
15
+ display_name
16
+ end
17
+
18
+ def self.get_minimum_deployment_macos_version(project_path, workspace)
19
+ _, target = get_app_target(project_path, workspace)
20
+ config = target.build_configurations.first
21
+ config.build_settings['MACOSX_DEPLOYMENT_TARGET']
22
+ end
23
+
24
+ def self.get_project_version(project_path, workspace_path)
25
+ _, target = get_app_target(project_path, workspace_path)
26
+ target.build_configurations.first.build_settings
27
+ [build_settings['MARKETING_VERSION'], build_settings['CURRENT_PROJECT_VERSION']]
28
+ end
29
+
30
+ def self.update_project_version(project_path, workspace_path)
31
+ project, target = get_app_target(project_path, workspace_path)
32
+ target.build_configurations.each do |config|
33
+ config.build_settings['MARKETING_VERSION'] = Env.variables.marketing_version
34
+ config.build_settings['CURRENT_PROJECT_VERSION'] = Env.variables.current_project_version
35
+ end
36
+ project.save
37
+
38
+ puts_if_verbose "Successfully updated the project version to #{Env.variables.marketing_version} and saved the project."
39
+ end
40
+
41
+ def self.get_app_target(project_path, workspace_path)
42
+ project = Xcodeproj::Project.open(project_path) if project_path
43
+ workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspace_path) if workspace_path
44
+
45
+ target = project.targets.find { |t| t.name == Env.variables.scheme } if project
46
+
47
+ # If workspace is used, find the project containing the scheme
48
+ if workspace && !target
49
+ workspace.file_references.each do |file_reference|
50
+ project_path = File.join(File.dirname(workspace_path), file_reference.path)
51
+ project = Xcodeproj::Project.open(project_path)
52
+ target = project.targets.find { |t| t.name == Env.variables.scheme }
53
+ break if target
54
+ end
55
+ end
56
+
57
+ raise "Target not found for scheme #{Env.variables.scheme_name}".red unless target
58
+
59
+ [project, target]
60
+ end
61
+
62
+ def self.check_sparkle_configuration_existence(project_path, workspace_path)
63
+ project, target = get_app_target(project_path, workspace_path)
64
+ info_plist_file = project.files.find { |f| f.path.end_with?('Info.plist') }
65
+ raise 'Info.plist not found in the project' unless info_plist_file
66
+
67
+ info_plist_path = File.join(Env.variables.project_directory_path, "#{target.name}/#{info_plist_file.path}")
68
+ info_plist = Xcodeproj::Plist.read_from_path(info_plist_path)
69
+
70
+ raise 'Info.plist does not contain the needed Sparkle configuration: SUFeedURL'.red if info_plist['SUFeedURL'].nil?
71
+ return unless info_plist['SUPublicEDKey'].nil?
72
+
73
+ raise 'Info.plist does not contain the needed Sparkle configuration: SUPublicEDKey'.red
74
+ end
75
+ end
@@ -0,0 +1,73 @@
1
+ require 'optparse'
2
+ require 'securerandom'
3
+ require_relative 'packaging'
4
+ require_relative 'distribution'
5
+ require_relative 'helpers/build_directory_helpers'
6
+ require_relative 'helpers/commands_helpers'
7
+ require_relative 'helpers/keychain_helpers'
8
+ require_relative 'helpers/xcodeproj_helpers'
9
+ require_relative 'helpers/puts_helpers'
10
+ require_relative 'environment/environment'
11
+
12
+ def extract_options
13
+ options = {}
14
+
15
+ OptionParser.new do |opts|
16
+ opts.banner = 'Usage: automate-sparkle.rb [options]'
17
+
18
+ opts.on('--project-path PATH', 'Path to the Xcode project') do |path|
19
+ options[:project_path] = path
20
+ end
21
+
22
+ opts.on('--workspace-path PATH', 'Path to the Xcode workspace') do |path|
23
+ options[:workspace_path] = path
24
+ end
25
+
26
+ env_description = 'Environment to load (aka. local, production)'
27
+ env_description += ' it will look inside the Xcode project/workspace path'
28
+ env_description += ' for a file named `.autosparkle.env.<environment>`'
29
+
30
+ opts.on('--env ENVIRONMENT', env_description) do |env|
31
+ options[:env] = env
32
+ end
33
+
34
+ opts.on('--env-file PATH', 'Path to the environment file') do |path|
35
+ options[:env_file] = path
36
+ end
37
+
38
+ opts.on('-v', 'Enable verbose mode') do |verbose|
39
+ Env.verbose_enabled = verbose
40
+ end
41
+ end.parse!
42
+
43
+ # Check if both project path and environment are specified
44
+ unless (options[:workspace_path] || options[:project_path]) && (options[:env] || options[:env_file])
45
+ raise 'Xcode Project/Workspace path and Environment/Environment file are required.'
46
+ end
47
+
48
+ options
49
+ end
50
+
51
+ options = extract_options
52
+ Env.initialize(options)
53
+
54
+ Xcodeproj.check_sparkle_configuration_existence(options[:project_path], options[:workspace_path])
55
+
56
+ BuildDirectory.create_build_directory
57
+
58
+ puts "Automating the delivery of #{Env.variables.app_display_name} version #{Env.variables.marketing_version} (#{Env.variables.current_project_version})..."
59
+ dmg_path = nil
60
+ with_temporary_keychain do |keychain_info|
61
+ exported_app_path = Packaging.archive_and_sign(
62
+ keychain_info[:application_cert_name],
63
+ keychain_info[:application_team_id],
64
+ options
65
+ )
66
+ dmg_path = Packaging.create_and_sign_dmg(
67
+ exported_app_path,
68
+ keychain_info[:application_cert_name],
69
+ keychain_info[:keychain_path]
70
+ )
71
+ end
72
+
73
+ Distribution.upload_update(dmg_path)
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/build_directory_helpers'
4
+ require_relative 'helpers/constants'
5
+ require_relative 'helpers/commands_helpers'
6
+ require_relative 'helpers/dmg_helpers'
7
+ require_relative 'helpers/puts_helpers'
8
+ require_relative 'environment/environment'
9
+
10
+ # Packaging module to archive and sign the app
11
+ module Packaging
12
+ def self.archive_and_sign(
13
+ application_cert_name,
14
+ application_cert_team_id,
15
+ options
16
+ )
17
+
18
+ puts_title 'Archiving and signing the app'
19
+ app_archive_path = BuildDirectory.new_path("#{Env.variables.app_display_name}.xcarchive")
20
+ archive_command = 'xcodebuild clean analyze archive'
21
+ archive_command += " -scheme #{Env.variables.scheme}"
22
+ archive_command += " -archivePath '#{app_archive_path}'"
23
+ archive_command += " CODE_SIGN_IDENTITY='#{application_cert_name}'"
24
+ archive_command += " DEVELOPMENT_TEAM='#{application_cert_team_id}'"
25
+ archive_command += " -configuration #{Env.variables.configuration}"
26
+ archive_command += " OTHER_CODE_SIGN_FLAGS='--timestamp --options=runtime'"
27
+
28
+ archive_command += if options[:workspace_path]
29
+ " -workspace #{options[:workspace_path]}"
30
+ else
31
+ " -project #{options[:project_path]}"
32
+ end
33
+
34
+ execute_command(archive_command)
35
+
36
+ puts_title 'Exporting the app'
37
+ exported_app_path = export_app(app_archive_path, application_cert_name, application_cert_team_id)
38
+
39
+ puts_title 'Signing the Sparkle framework'
40
+ sign_sparkle_framework(
41
+ exported_app_path,
42
+ application_cert_name
43
+ )
44
+
45
+ exported_app_path
46
+ end
47
+
48
+ def self.export_app(app_archive_path, application_cert_name, team_id)
49
+ puts_if_verbose 'Exporting the app...'
50
+ export_options = {
51
+ signingStyle: 'automatic',
52
+ method: 'developer-id',
53
+ teamID: team_id,
54
+ signingCertificate: application_cert_name,
55
+ destination: 'export'
56
+ }
57
+
58
+ export_options_file = BuildDirectory.new_file('exportOptions.plist')
59
+ plist_content = export_options.to_plist
60
+ export_options_file.write(plist_content)
61
+ export_options_file.close
62
+
63
+ puts_if_verbose "exportOptions.plist:\n#{plist_content}"
64
+
65
+ # Create temporary directory for the exported app
66
+ export_app_dir_path = BuildDirectory.new_directory('exported_app')
67
+
68
+ # Construct the export command
69
+ export_command = "xcodebuild -exportArchive -archivePath \"#{app_archive_path}\""
70
+ export_command += " -exportPath \"#{export_app_dir_path}\""
71
+ export_command += " -exportOptionsPlist \"#{export_options_file.path}\""
72
+ execute_command(export_command)
73
+
74
+ "#{export_app_dir_path}/#{Env.variables.app_display_name}.app"
75
+ end
76
+
77
+ def self.create_and_sign_dmg(exported_app_path, application_cert_name, keychain_path)
78
+ puts_title "Creating #{Env.variables.app_display_name}.dmg"
79
+ puts_if_verbose 'Creating a DMG for the app...'
80
+ dmg_path = DMG.create(exported_app_path)
81
+
82
+ # Sign the DMG
83
+ puts_if_verbose 'Signing the DMG...'
84
+ execute_command("codesign --force --sign \"#{application_cert_name}\" --timestamp --options runtime \"#{dmg_path}\"")
85
+
86
+ # Notarize the DMG
87
+ puts_if_verbose 'Notarizing the DMG...'
88
+ execute_command("xcrun notarytool submit \"#{dmg_path}\" --keychain-profile \"#{Constants::NOTARIZE_KEYCHAIN_PROFILE}\" --keychain \"#{keychain_path}\" --wait")
89
+
90
+ # Staple the notarization ticket
91
+ puts_if_verbose 'Stapling the notarization ticket...'
92
+ execute_command("xcrun stapler staple \"#{dmg_path}\"")
93
+
94
+ dmg_path
95
+ end
96
+
97
+ def self.sign_sparkle_framework(exported_app_path, application_cert_name)
98
+ sparkle_auto_update_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework/AutoUpdate"
99
+ codesign('Signing Sparkle AutoUpdate...', application_cert_name, sparkle_auto_update_path)
100
+
101
+ sparkle_updater_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework/Updater.app"
102
+ codesign('Signing Sparkle Updater...', application_cert_name, sparkle_updater_path)
103
+
104
+ sparkle_installer_xpc_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework/XPCServices/Installer.xpc/Contents/MacOS/Installer"
105
+ codesign('Signing Sparkle Installer XPC Service...', application_cert_name, sparkle_installer_xpc_path)
106
+
107
+ sparkle_downloader_xpc_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
108
+ codesign('Signing Sparkle Downloader XPC Service...', application_cert_name, sparkle_downloader_xpc_path)
109
+
110
+ sparkle_framework_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework"
111
+ codesign('Signing Sparkle framework...', application_cert_name, sparkle_framework_path)
112
+ end
113
+
114
+ def self.codesign(title, application_cert_name, path)
115
+ puts_if_verbose title
116
+ execute_command("codesign -f -o runtime --timestamp -s \"#{application_cert_name}\" \"#{path}\"")
117
+ end
118
+ end
Binary file
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ set -e
3
+ set -o pipefail
4
+ if [ "$#" -ne 2 ]; then
5
+ echo "Usage: $0 update_archive_file dsa_priv.pem"
6
+ echo "This is an old DSA signing script for deprecated DSA keys."
7
+ echo "Do not use this for new applications."
8
+ exit 1
9
+ fi
10
+ openssl=/usr/bin/openssl
11
+ version=`$openssl version`
12
+ if [[ $version =~ "OpenSSL 0.9" ]]; then
13
+ # pre-10.13 system: Fall back to OpenSSL DSS1 digest because it does not like the -sha1 option
14
+ $openssl dgst -sha1 -binary < "$1" | $openssl dgst -dss1 -sign "$2" | $openssl enc -base64
15
+ else
16
+ # 10.13 and later: Use LibreSSL SHA1 digest
17
+ $openssl dgst -sha1 -binary < "$1" | $openssl dgst -sha1 -sign "$2" | $openssl enc -base64
18
+ fi
Binary file
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-s3'
4
+ require_relative 'storage'
5
+ require_relative '../environment/environment'
6
+ require_relative '../environment/variables/aws_s3_environment_variables'
7
+ require_relative '../helpers/puts_helpers'
8
+
9
+ # AwsS3Storage class to upload the updated version of the app
10
+ class AwsS3Storage
11
+ include Storage
12
+ def initialize
13
+ @variables = AwsS3EnvironmentVariables.new
14
+
15
+ credentials = Aws::Credentials.new(@variables.access_key, @variables.secret_access_key)
16
+ Aws.config.update({
17
+ region: @variables.region,
18
+ credentials: credentials
19
+ })
20
+ s3 = Aws::S3::Resource.new
21
+ @bucket = s3.bucket(@variables.bucket_name)
22
+ super
23
+ end
24
+
25
+ def upload(pkg_path, appcast_path)
26
+ appcast_object = @bucket.object('appcast.xml')
27
+ appcast_object.upload_file(appcast_path)
28
+
29
+ puts_if_verbose "Uploaded the appcast file to the bucket #{@variables.bucket_name}"
30
+
31
+ version_object = @bucket.object(update_file_destination_path)
32
+ version_object.upload_file(pkg_path)
33
+
34
+ puts_if_verbose "Uploaded version #{Env.variables.marketing_version} to the bucket #{@variables.bucket_name}"
35
+ rescue StandardError => e
36
+ raise "Failed to upload file: #{e.message}"
37
+ end
38
+
39
+ def deployed_appcast_xml
40
+ appcast_object = @bucket.object('appcast.xml')
41
+ appcast_object.get.body.read
42
+ rescue StandardError
43
+ nil
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is used as an interface for the storage classes
4
+ # It contains the methods that the storage classes should implement
5
+ # A storage can be for example Google Drive, Azure Blob Storage, AWS S3, etc.
6
+ # The update_file_destination_path method will return the path where the file should be uploaded
7
+ # The upload method is used to upload the package and the appcast file to the storage
8
+ module Storage
9
+ def update_file_destination_path
10
+ "#{Env.variables.marketing_version}/#{Env.variables.app_display_name}.dmg"
11
+ end
12
+
13
+ def upload(pkg_path, appcast_path)
14
+ raise NotImplementedError, "This #{self.class} cannot respond to:"
15
+ end
16
+
17
+ def deployed_appcast_xml
18
+ raise NotImplementedError, "This #{self.class} cannot respond to:"
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: autosparkle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.8
5
+ platform: ruby
6
+ authors:
7
+ - Hadi Dbouk
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-s3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.152.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.152.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: colorize
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: dotenv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.1.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.1.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.16.6
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.16.6
69
+ - !ruby/object:Gem::Dependency
70
+ name: xcodeproj
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.24.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.24.0
83
+ description: Autosparkle is a command-line tool that automates the delivery of macOS
84
+ apps outside the App Store. It is designed to be used in CI/CD pipelines to automate
85
+ the process of archiving, signing, notarizing, and distributing macOS apps.
86
+ email:
87
+ - hadiidbouk@gmail.com
88
+ executables:
89
+ - autosparkle
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - README.md
94
+ - bin/autosparkle
95
+ - lib/autosparkle/distribution.rb
96
+ - lib/autosparkle/environment/environment.rb
97
+ - lib/autosparkle/environment/variables/aws_s3_environment_variables.rb
98
+ - lib/autosparkle/environment/variables/base_environment_variables.rb
99
+ - lib/autosparkle/environment/variables/default_environment_variables.rb
100
+ - lib/autosparkle/helpers/appcast_helpers.rb
101
+ - lib/autosparkle/helpers/build_directory_helpers.rb
102
+ - lib/autosparkle/helpers/commands_helpers.rb
103
+ - lib/autosparkle/helpers/constants.rb
104
+ - lib/autosparkle/helpers/dmg_helpers.rb
105
+ - lib/autosparkle/helpers/keychain_helpers.rb
106
+ - lib/autosparkle/helpers/puts_helpers.rb
107
+ - lib/autosparkle/helpers/version_helpers.rb
108
+ - lib/autosparkle/helpers/xcodeproj_helpers.rb
109
+ - lib/autosparkle/main.rb
110
+ - lib/autosparkle/packaging.rb
111
+ - lib/autosparkle/sparkle/BinaryDelta
112
+ - lib/autosparkle/sparkle/generate_appcast
113
+ - lib/autosparkle/sparkle/generate_keys
114
+ - lib/autosparkle/sparkle/old_dsa_scripts/sign_update
115
+ - lib/autosparkle/sparkle/sign_update
116
+ - lib/autosparkle/storages/aws_s3_storage.rb
117
+ - lib/autosparkle/storages/storage.rb
118
+ homepage: https://github.com/hadiidbouk/autosparkle
119
+ licenses:
120
+ - MIT
121
+ metadata:
122
+ homepage_uri: https://github.com/hadiidbouk/autosparkle
123
+ source_code_uri: https://github.com/hadiidbouk/autosparkle
124
+ changelog_uri: https://github.com/hadiidbouk/autosparkle/blob/main/CHANGELOG.md
125
+ rubygems_mfa_required: 'true'
126
+ post_install_message: |+
127
+ ###################################### AUTOSPAKRLE ######################################
128
+
129
+ Thank you for installing AutoSparkle!
130
+
131
+ To use AutoSparkle in your terminal, you need to update your shell configuration files.
132
+ Add the following line to your shell configuration file (e.g., ~/.bashrc, ~/.zshrc):
133
+ export PATH="<<YOUR_GEMS_DIR>>/autosparkle-x.x.x/bin:$PATH"
134
+
135
+ After updating your shell configuration file, run the following command:
136
+ source ~/.bashrc or source ~/.zshrc
137
+
138
+ For more information, visit the AutoSparkle GitHub repository:
139
+ https://github.com/hadiidbouk/autosparkle
140
+
141
+ Enjoy using AutoSparkle!
142
+
143
+ #########################################################################################
144
+
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 2.5.0
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubygems_version: 3.5.9
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Automate macOS app delivery outside the App Store
163
+ test_files: []