autosparkle 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []