autosparkle 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +45 -6
- data/lib/autosparkle/environment/environment.rb +65 -19
- data/lib/autosparkle/environment/variables/default_environment_variables.rb +2 -3
- data/lib/autosparkle/helpers/constants.rb +3 -0
- data/lib/autosparkle/helpers/dmg_helpers.rb +28 -7
- data/lib/autosparkle/helpers/keychain_helpers.rb +30 -30
- data/lib/autosparkle/helpers/xcodeproj_helpers.rb +3 -1
- data/lib/autosparkle/main.rb +138 -49
- data/lib/autosparkle/metadata.rb +8 -0
- data/lib/autosparkle/packaging.rb +54 -28
- data/lib/autosparkle/resources/default-dmg-background.png +0 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ecc21f98e6511792838c080397ff61a8f35abb44621e2ac8b43165b6a893d0f
|
4
|
+
data.tar.gz: '0239c58273e8b1d7a54d91cbea17a7036af6cec889eafbf77d5b14e8ea182372'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8ef56bb0a64677667ed1e8ab43f704f2c113a6bf0d97c8ddbf7090a42159a8681417945a7b3339b73b66c5dffe308cdda571a874e655351136fa67174a2817a
|
7
|
+
data.tar.gz: 756e49be1e6843e182f46606c96450042c7dfaac4b3d398d6066340456bb202571300e2352e040156cb0ad52af7e4f68cdfb31bbbcac56a3b10ee79d82c79d9c
|
data/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# autosparkle
|
2
|
+
![GitHub Release](https://img.shields.io/github/v/release/hadiidbouk/autosparkle?style=flat&label=version)
|
3
|
+
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/hadiidbouk/autosparkle/ci.yml)
|
2
4
|
|
3
5
|
![autosparkle main image](resources/readme-main-image.jpg)
|
4
6
|
|
@@ -59,13 +61,50 @@ autosparkle is a Ruby command line tool that automates the delivery of your macO
|
|
59
61
|
```bash
|
60
62
|
git clone https://github.com/hadiidbouk/autosparkle.git
|
61
63
|
```
|
62
|
-
2.
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
2. Execute the Ruby script `autosparkle.rb`, check the [Usage](#usage) section below.
|
65
|
+
|
66
|
+
## Usage
|
67
|
+
|
68
|
+
There are two ways to utilize autosparkle. The first option is to automate the entire process by using the `automate` command. Alternatively, you can select any of the following commands (`export`, `package`, `distribute`) and build your own solution around them.
|
69
|
+
|
70
|
+
|
71
|
+
All of these commands require you to provide the environment file or its name. If you use the `automate` command or the `export` command, which require a project/workspace path, you can simply pass the environment name to the `--env` option. autosparkle will then search for a file named `.env.autosparkle.<<YOUR_ENV_NAME>>` in your project directory (where your Xcode project/workspace is located). For example, if you pass `--env local`, autosparkle will search for `.env.autosparkle.local`.
|
72
|
+
|
73
|
+
It is **recommended** to use the `automate` command for several reasons:
|
74
|
+
|
75
|
+
1. It automatically handles the app versioning, ensuring consistency between the Info.plist and the `appcast.xml`.
|
76
|
+
|
77
|
+
2. The custom keychain is created once for all the signing steps.
|
78
|
+
|
79
|
+
3. All the generated files are conveniently located in one place at `~/Library/Developer/autosparkle/build`. When using the commands separately, each command will override any existing files in the build directory.
|
80
|
+
|
66
81
|
|
67
|
-
You can also pass your workspace path using `--workspace-path` and specify the environment path using `--env-file`.
|
68
82
|
|
83
|
+
You can explore the functionality of each command by using the `--help` flag. For instance:
|
84
|
+
```bash
|
85
|
+
autosparkle distribute --help
|
86
|
+
```
|
87
|
+
|
88
|
+
Output:
|
89
|
+
```bash
|
90
|
+
distribute
|
91
|
+
|
92
|
+
Usage: autosparkle distribute [options]
|
93
|
+
|
94
|
+
Distribute your package to the specified storage and update the appcast.xml file
|
95
|
+
|
96
|
+
Options:
|
97
|
+
--dmg-path PATH Path of the DMG file to be distributed
|
98
|
+
--app-display-name NAME Name of the app inside the DMG without the .app extension
|
99
|
+
--marketing-version VERSION Marketing version of the app
|
100
|
+
--current-project-version VERSION Current project version of the app
|
101
|
+
--minimum-macos-version VERSION Minimum macOS version required to run the app, defaults to 14.0
|
102
|
+
```
|
103
|
+
|
104
|
+
|
105
|
+
> **Note:**
|
106
|
+
>
|
107
|
+
> When using the **distribute** command, autosparkle will automatically calculate the new marketing version and the current project version from the uploaded appcast file (if it exists). If you want to override these values, make sure to pass them using the `--marketing-version` and `--current-project-version` options. Additionally, make sure to set them in your app's Info.plist before exporting the app.
|
69
108
|
|
70
109
|
## Environment
|
71
110
|
autosparkle utilizes the [dotenv](https://github.com/bkeepers/dotenv) gem to manage environment variables.
|
@@ -89,7 +128,7 @@ Below are the list of environment variables used by autosparkle:
|
|
89
128
|
| APP_SPECIFIC_PASSWORD | The App specific password that will be used in the notarize step | Yes | |
|
90
129
|
| DEVELOPER_ID_APPLICATION_BASE64 | The Developer ID Application base64 certificate | Yes | |
|
91
130
|
| DEVELOPER_ID_APPLICATION_PASSWORD | The Developer ID Application base64 certificate password | Yes | |
|
92
|
-
| DMG_BACKGROUND_IMAGE | The DMG background image
|
131
|
+
| DMG_BACKGROUND_IMAGE | The path to the DMG background image. Make sure to use an image with the same width and height as the window to ensure a proper fit. | No | [View it here](lib/autosparkle/resources/default-dmg-background.png) |
|
93
132
|
| DMG_WINDOW_WIDTH | The DMG "Drag to Applications" window width | Yes | |
|
94
133
|
| DMG_WINDOW_HEIGHT | The DMG "Drag to Applications" window height | Yes | |
|
95
134
|
| DMG_ICON_SIZE | The icon size of the app and the Applications folder in the window | Yes | |
|
@@ -7,6 +7,13 @@ require_relative '../helpers/puts_helpers'
|
|
7
7
|
require_relative '../storages/aws_s3_storage'
|
8
8
|
require_relative 'variables/default_environment_variables'
|
9
9
|
|
10
|
+
module Command
|
11
|
+
EXPORT = 0
|
12
|
+
PACKAGE = 1
|
13
|
+
DISTRIBUTE = 2
|
14
|
+
AUTOMATE = 3
|
15
|
+
end
|
16
|
+
|
10
17
|
# Environment module to load the environment variables
|
11
18
|
# It contains the app state
|
12
19
|
module Env
|
@@ -17,27 +24,69 @@ module Env
|
|
17
24
|
class << self
|
18
25
|
attr_accessor :variables, :storage, :verbose_enabled
|
19
26
|
|
20
|
-
def initialize(options)
|
21
|
-
project_path = options
|
22
|
-
workspace_path = options
|
23
|
-
project_directory_path = File.dirname(workspace_path || project_path)
|
27
|
+
def initialize(options, command)
|
28
|
+
project_path = options.project_path
|
29
|
+
workspace_path = options.workspace_path
|
24
30
|
|
25
|
-
|
31
|
+
project_directory_path = File.dirname(workspace_path || project_path) if workspace_path || project_path
|
32
|
+
ENV['PROJECT_DIRECTORY_PATH'] = project_directory_path
|
26
33
|
|
27
|
-
|
28
|
-
@storage = retrieve_storage
|
34
|
+
load_environment(project_directory_path, options.env)
|
29
35
|
|
30
|
-
|
31
|
-
set_up_app_versions(project_path, workspace_path)
|
36
|
+
@variables = DefaultEnvironmentVariables.new
|
32
37
|
|
33
|
-
|
38
|
+
case command
|
39
|
+
when Command::EXPORT
|
40
|
+
initialize_export(options)
|
41
|
+
when Command::PACKAGE
|
42
|
+
initialize_package(options)
|
43
|
+
when Command::DISTRIBUTE
|
44
|
+
initialize_distribute(options)
|
45
|
+
when Command::AUTOMATE
|
46
|
+
initialize_automate(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
puts_if_verbose "Running the script with the #{options.env} environment...\n"
|
34
50
|
end
|
35
51
|
end
|
36
52
|
|
37
|
-
def self.
|
38
|
-
|
39
|
-
|
53
|
+
def self.initialize_export(options)
|
54
|
+
retrieve_variables_from_xcode(options.project_path, options.workspace_path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.initialize_package(options)
|
58
|
+
ENV['APP_DISPLAY_NAME'] = options.app_display_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.initialize_distribute(options)
|
62
|
+
ENV['APP_DISPLAY_NAME'] = options.app_display_name
|
63
|
+
ENV['MINIMUM_MACOS_VERSION'] = options.minimum_macos_version
|
64
|
+
|
65
|
+
@storage = retrieve_storage
|
66
|
+
retreive_versions_variables_form_appcast(options)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.initialize_automate(options)
|
70
|
+
retrieve_variables_from_xcode(options.project_path, options.workspace_path)
|
71
|
+
|
72
|
+
@storage = retrieve_storage
|
73
|
+
retreive_versions_variables_form_appcast(options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load_environment(project_directory_path, env)
|
77
|
+
env_is_file = File.file?(env)
|
78
|
+
if !project_directory_path && !env_is_file
|
79
|
+
raise 'Cannot find the project directory path to load the environment variables'
|
80
|
+
end
|
81
|
+
|
82
|
+
env_file_path = if env_is_file
|
83
|
+
env
|
84
|
+
else
|
85
|
+
File.join(project_directory_path, ".env.autosparkle.#{env}")
|
86
|
+
end
|
40
87
|
|
88
|
+
ENV['ENV_FILE_PATH'] = env_file_path
|
89
|
+
puts_if_verbose "Loading the environment variables from #{env_file_path}..."
|
41
90
|
Dotenv.load(env_file_path)
|
42
91
|
end
|
43
92
|
|
@@ -61,13 +110,10 @@ module Env
|
|
61
110
|
ENV['APP_DISPLAY_NAME'] = Xcodeproj.get_app_display_name(project_path, workspace_path)
|
62
111
|
end
|
63
112
|
|
64
|
-
def self.
|
113
|
+
def self.retreive_versions_variables_form_appcast(options)
|
65
114
|
marketing_version, current_project_version = AppcastXML.retreive_versions(@storage.deployed_appcast_xml)
|
66
115
|
|
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)
|
116
|
+
ENV['MARKETING_VERSION'] = options.marketing_version || marketing_version
|
117
|
+
ENV['CURRENT_PROJECT_VERSION'] = options.current_project_version || current_project_version
|
72
118
|
end
|
73
119
|
end
|
@@ -6,8 +6,9 @@ require_relative 'base_environment_variables'
|
|
6
6
|
|
7
7
|
# A class to load the default environment variables
|
8
8
|
class DefaultEnvironmentVariables < BaseEnvironmentVariables
|
9
|
-
def initialize
|
9
|
+
def initialize
|
10
10
|
super({
|
11
|
+
env_file_path: 'ENV_FILE_PATH',
|
11
12
|
project_directory_path: 'PROJECT_DIRECTORY_PATH',
|
12
13
|
scheme: 'SCHEME',
|
13
14
|
configuration: 'CONFIGURATION',
|
@@ -29,7 +30,5 @@ class DefaultEnvironmentVariables < BaseEnvironmentVariables
|
|
29
30
|
dmg_window_width: 'DMG_WINDOW_WIDTH',
|
30
31
|
dmg_window_height: 'DMG_WINDOW_HEIGHT'
|
31
32
|
})
|
32
|
-
|
33
|
-
ENV['PROJECT_DIRECTORY_PATH'] = project_directory_path
|
34
33
|
end
|
35
34
|
end
|
@@ -4,4 +4,7 @@
|
|
4
4
|
module Constants
|
5
5
|
NOTARIZE_KEYCHAIN_PROFILE = 'autosparkle.keychain.notarize.profile'
|
6
6
|
AUTOSPARKLE_BUILD_DIRECTORY_PATH = '~/Library/Developer/autosparkle/build'
|
7
|
+
KEYCHAIN_NAME = 'temporary.autosparkle.keychain'
|
8
|
+
KEYCHAIN_PATH = "~/Library/Keychains/#{KEYCHAIN_NAME}-db"
|
9
|
+
KEYCHAIN_PASSWORD = 'autosparkle'
|
7
10
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'fileutils'
|
4
|
+
require 'xcodeproj'
|
4
5
|
require_relative 'build_directory_helpers'
|
5
6
|
require_relative 'puts_helpers'
|
6
7
|
require_relative '../environment/environment'
|
@@ -9,9 +10,10 @@ require_relative '../environment/environment'
|
|
9
10
|
module DMG
|
10
11
|
def self.create(app_path)
|
11
12
|
volume_name = Env.variables.app_display_name
|
12
|
-
background_image_path = "#{Env.variables.project_directory_path}/#{Env.variables.dmg_background_image}"
|
13
13
|
|
14
|
-
|
14
|
+
background_image_path = dmg_background_image_path
|
15
|
+
volume_size = calculate_volume_size(app_path, background_image_path)
|
16
|
+
dmg_path = create_blank_dmg(volume_size, volume_name)
|
15
17
|
mount(dmg_path, volume_name)
|
16
18
|
copy_app_and_set_symbolic_link(app_path, volume_name)
|
17
19
|
copy_background_image(background_image_path, volume_name)
|
@@ -20,16 +22,32 @@ module DMG
|
|
20
22
|
create_read_only_dmg(dmg_path)
|
21
23
|
end
|
22
24
|
|
23
|
-
def self.
|
25
|
+
def self.dmg_background_image_path
|
26
|
+
background_image_path = Env.variables.dmg_background_image
|
27
|
+
final_background_image_path = if background_image_path.start_with?('~')
|
28
|
+
File.expand_path(background_image_path)
|
29
|
+
elsif background_image_path.start_with?('/')
|
30
|
+
background_image_path
|
31
|
+
else
|
32
|
+
File.expand_path(background_image_path, Env.variables.env_file_path)
|
33
|
+
end
|
34
|
+
|
35
|
+
raise 'DMG background image not found' unless !background_image_path || File.exist?(final_background_image_path)
|
36
|
+
|
37
|
+
default_dmg_background_path = File.join(File.dirname(__dir__), 'resources', 'default-dmg-background.png')
|
38
|
+
final_background_image_path ||= default_dmg_background_path
|
39
|
+
final_background_image_path
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.create_blank_dmg(volume_size, volume_name)
|
24
43
|
puts_if_verbose 'Creating a blank DMG...'
|
25
|
-
volume_size = volume_size(app_path, background_image_path)
|
26
44
|
uuid = `uuidgen`.strip
|
27
45
|
dmg_path = BuildDirectory.new_path("#{Env.variables.app_display_name}-#{uuid}.dmg")
|
28
46
|
execute_command("hdiutil create -size #{volume_size}m -fs HFS+ -volname '#{volume_name}' -ov '#{dmg_path}'")
|
29
47
|
dmg_path
|
30
48
|
end
|
31
49
|
|
32
|
-
def self.
|
50
|
+
def self.calculate_volume_size(app_path, background_image_path)
|
33
51
|
app_size_kb = `du -sk "#{app_path}"`.split("\t").first.to_i
|
34
52
|
background_size_kb = File.size(background_image_path) / 1024
|
35
53
|
buffer_size_kb = 20 * 1024
|
@@ -51,7 +69,10 @@ module DMG
|
|
51
69
|
def self.copy_background_image(background_image_path, volume_name)
|
52
70
|
puts_if_verbose 'Copying the background image to the DMG...'
|
53
71
|
FileUtils.mkdir_p("/Volumes/#{volume_name}/.background")
|
54
|
-
|
72
|
+
|
73
|
+
background_image_extension = File.extname(background_image_path)
|
74
|
+
FileUtils.cp(background_image_path,
|
75
|
+
"/Volumes/#{volume_name}/.background/dmg-background#{background_image_extension}")
|
55
76
|
end
|
56
77
|
|
57
78
|
def self.customize_dmg_appearence(volume_name)
|
@@ -78,7 +99,7 @@ module DMG
|
|
78
99
|
set the bounds of container window to {0, 0, #{window_width}, #{window_height}}
|
79
100
|
set arrangement of icon view options of container window to not arranged
|
80
101
|
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
|
102
|
+
set background picture of icon view options of container window to file ".background:dmg-background.png"
|
82
103
|
set position of item "#{Env.variables.app_display_name}.app" of container window to {#{app_x_position}, #{item_y_position}}
|
83
104
|
set position of item "Applications" of container window to {#{applications_x_position}, #{item_y_position}}
|
84
105
|
close
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'securerandom'
|
2
4
|
require 'open3'
|
3
5
|
require 'base64'
|
@@ -12,36 +14,31 @@ require_relative 'puts_helpers'
|
|
12
14
|
# The temporary keychain will be deleted after the block has been executed.
|
13
15
|
#
|
14
16
|
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
17
|
original_keychain_list = `security list-keychains`.strip.split("\n").map(&:strip)
|
20
18
|
default_keychain = execute_command('security default-keychain')
|
21
19
|
default_keychain_path = default_keychain.gsub(/"(.*)"/, '\1').strip
|
22
20
|
|
23
|
-
delete_temporary_keychain_if_needed
|
21
|
+
delete_temporary_keychain_if_needed
|
24
22
|
|
25
23
|
begin
|
26
24
|
# Create a temporary keychain
|
27
|
-
create_temporary_keychain(
|
28
|
-
import_certificates_in_temporary_keychain
|
25
|
+
create_temporary_keychain(original_keychain_list)
|
26
|
+
import_certificates_in_temporary_keychain
|
29
27
|
execute_command("security set-key-partition-list -S apple-tool:,apple:,codesign: \\
|
30
|
-
-s -k \"#{
|
28
|
+
-s -k \"#{Constants::KEYCHAIN_PASSWORD}\" #{Constants::KEYCHAIN_PATH}")
|
31
29
|
|
32
30
|
# Fetch the certificate names and team ids from the temporary keychain
|
33
|
-
application_cert_name, application_team_id = fetch_application_certificate_info
|
34
|
-
store_notarization_credentials(
|
31
|
+
application_cert_name, application_team_id = fetch_application_certificate_info
|
32
|
+
store_notarization_credentials(application_team_id)
|
35
33
|
|
36
34
|
keychain_info = {
|
37
|
-
keychain_path: keychain_path,
|
38
35
|
application_cert_name: application_cert_name,
|
39
36
|
application_team_id: application_team_id
|
40
37
|
}
|
41
38
|
yield(keychain_info) if block_given?
|
42
39
|
ensure
|
43
40
|
puts_if_verbose 'Ensuring cleanup of temporary keychain...'
|
44
|
-
delete_temporary_keychain_if_needed
|
41
|
+
delete_temporary_keychain_if_needed
|
45
42
|
|
46
43
|
# Reset the keychain to the default
|
47
44
|
execute_command("security list-keychains -s #{original_keychain_list.join(' ')}")
|
@@ -51,23 +48,23 @@ end
|
|
51
48
|
|
52
49
|
private
|
53
50
|
|
54
|
-
def create_temporary_keychain(
|
55
|
-
execute_command("security create-keychain -p \"#{
|
56
|
-
execute_command("security unlock-keychain -p \"#{
|
57
|
-
execute_command("security list-keychains -d user -s #{(original_keychain_list + [
|
58
|
-
execute_command("security default-keychain -s #{
|
51
|
+
def create_temporary_keychain(original_keychain_list)
|
52
|
+
execute_command("security create-keychain -p \"#{Constants::KEYCHAIN_PASSWORD}\" #{Constants::KEYCHAIN_PATH}")
|
53
|
+
execute_command("security unlock-keychain -p \"#{Constants::KEYCHAIN_PASSWORD}\" #{Constants::KEYCHAIN_PATH}")
|
54
|
+
execute_command("security list-keychains -d user -s #{(original_keychain_list + [Constants::KEYCHAIN_PATH]).join(' ')}")
|
55
|
+
execute_command("security default-keychain -s #{Constants::KEYCHAIN_PATH}")
|
59
56
|
end
|
60
57
|
|
61
|
-
def store_notarization_credentials(
|
58
|
+
def store_notarization_credentials(application_team_id)
|
62
59
|
command = "xcrun notarytool store-credentials #{Constants::NOTARIZE_KEYCHAIN_PROFILE} \\
|
63
|
-
--keychain #{
|
60
|
+
--keychain #{Constants::KEYCHAIN_PATH} \\
|
64
61
|
--apple-id #{Env.variables.apple_id} \\
|
65
62
|
--team-id #{application_team_id} \\
|
66
63
|
--password #{Env.variables.app_specific_password}"
|
67
64
|
execute_command(command)
|
68
65
|
end
|
69
66
|
|
70
|
-
def import_certificates_in_temporary_keychain
|
67
|
+
def import_certificates_in_temporary_keychain
|
71
68
|
developer_id_application_p12 = Base64.decode64(Env.variables.developer_id_application_base64 || '')
|
72
69
|
|
73
70
|
# Create temporary files for the .p12 certificates
|
@@ -78,12 +75,11 @@ def import_certificates_in_temporary_keychain(keychain_path)
|
|
78
75
|
application_cert_file.close
|
79
76
|
|
80
77
|
# Import the certificates into the temporary keychain
|
81
|
-
import_file_to_keychain(
|
82
|
-
Env.variables.developer_id_application_password)
|
78
|
+
import_file_to_keychain(application_cert_file.path, Env.variables.developer_id_application_password)
|
83
79
|
end
|
84
80
|
|
85
|
-
def import_file_to_keychain(
|
86
|
-
command = "security import #{file_path} -k #{
|
81
|
+
def import_file_to_keychain(file_path, password)
|
82
|
+
command = "security import #{file_path} -k #{Constants::KEYCHAIN_PATH} -P #{password}"
|
87
83
|
command += ' -T /usr/bin/codesign'
|
88
84
|
command += ' -T /usr/bin/security'
|
89
85
|
command += ' -T /usr/bin/productbuild'
|
@@ -91,17 +87,21 @@ def import_file_to_keychain(keychain_path, file_path, password)
|
|
91
87
|
execute_command(command)
|
92
88
|
end
|
93
89
|
|
94
|
-
def fetch_certificate_info(
|
95
|
-
command = "security find-certificate -c \"#{certificate_type}\" #{
|
90
|
+
def fetch_certificate_info(certificate_type)
|
91
|
+
command = "security find-certificate -c \"#{certificate_type}\" #{Constants::KEYCHAIN_PATH}"
|
92
|
+
command += " | grep \"labl\" | sed -E 's/^.*\"labl\"<blob>=\"(.*)\".*/\\1/'"
|
96
93
|
name = `#{command}`.strip
|
97
94
|
team_id = name[/\(([^)]+)\)$/, 1]
|
98
95
|
[name, team_id]
|
99
96
|
end
|
100
97
|
|
101
|
-
def fetch_application_certificate_info
|
102
|
-
fetch_certificate_info(
|
98
|
+
def fetch_application_certificate_info
|
99
|
+
fetch_certificate_info('Developer ID Application')
|
103
100
|
end
|
104
101
|
|
105
|
-
def delete_temporary_keychain_if_needed
|
106
|
-
|
102
|
+
def delete_temporary_keychain_if_needed
|
103
|
+
return unless File.exist?(File.expand_path(Constants::KEYCHAIN_PATH.to_s))
|
104
|
+
|
105
|
+
execute_command("security delete-keychain #{Constants::KEYCHAIN_PATH}")
|
106
|
+
execute_command("rm -rf #{Constants::KEYCHAIN_PATH}")
|
107
107
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'colorize'
|
2
4
|
require 'xcodeproj'
|
3
5
|
require 'plist'
|
@@ -54,7 +56,7 @@ module Xcodeproj
|
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
57
|
-
raise "Target not found for scheme #{Env.variables.
|
59
|
+
raise "Target not found for scheme #{Env.variables.scheme}".red unless target
|
58
60
|
|
59
61
|
[project, target]
|
60
62
|
end
|
data/lib/autosparkle/main.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'commander/import'
|
4
|
+
require 'fileutils'
|
5
|
+
require_relative 'metadata'
|
3
6
|
require_relative 'packaging'
|
4
7
|
require_relative 'distribution'
|
5
8
|
require_relative 'helpers/build_directory_helpers'
|
@@ -7,67 +10,153 @@ require_relative 'helpers/commands_helpers'
|
|
7
10
|
require_relative 'helpers/keychain_helpers'
|
8
11
|
require_relative 'helpers/xcodeproj_helpers'
|
9
12
|
require_relative 'helpers/puts_helpers'
|
13
|
+
require_relative 'helpers/dmg_helpers'
|
10
14
|
require_relative 'environment/environment'
|
11
15
|
|
12
|
-
|
13
|
-
|
16
|
+
program :name, AutoSparkle::NAME
|
17
|
+
program :version, AutoSparkle::VERSION
|
18
|
+
program :description, AutoSparkle::DESCRIPTION
|
19
|
+
program :help_formatter, :compact
|
14
20
|
|
15
|
-
|
16
|
-
|
21
|
+
default_command :help
|
22
|
+
global_option('--env ENVIRONMENT', String, 'Environment to load (aka. local, production) or a path to the env file')
|
23
|
+
global_option('--verbose', 'Enable verbose mode') { Env.verbose_enabled = true }
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
command :export do |c|
|
26
|
+
c.syntax = "#{AutoSparkle::NAME} export [options]"
|
27
|
+
c.description = 'Archive and export the macOS app'
|
28
|
+
c.option('--project-path PATH', String, 'Path to the Xcode project')
|
29
|
+
c.option('--workspace-path PATH', String, 'Path to the Xcode workspace')
|
30
|
+
c.option('--skip-sparkle-steps', 'Skip the sparkle config check and signing the framework')
|
31
|
+
c.option('--output-dir PATH', String, 'Path to the output directory')
|
32
|
+
c.action do |_args, options|
|
33
|
+
options.default \
|
34
|
+
output_dir: Dir.pwd
|
21
35
|
|
22
|
-
|
23
|
-
|
24
|
-
|
36
|
+
raise 'env name/path is required.' unless options.env
|
37
|
+
raise 'Xcode Project/Workspace file is required.' unless options.workspace_path || options.project_path
|
38
|
+
raise 'Environment/Environment file is required.' unless options.env || options.env_file
|
25
39
|
|
26
|
-
|
27
|
-
|
28
|
-
env_description += ' for a file named `.autosparkle.env.<environment>`'
|
40
|
+
BuildDirectory.create_build_directory
|
41
|
+
Env.initialize(options, Command::EXPORT)
|
29
42
|
|
30
|
-
|
31
|
-
|
43
|
+
unless options.skip_sparkle_steps
|
44
|
+
Xcodeproj.check_sparkle_configuration_existence(
|
45
|
+
options.project_path,
|
46
|
+
options.workspace_path
|
47
|
+
)
|
32
48
|
end
|
33
49
|
|
34
|
-
|
35
|
-
|
36
|
-
|
50
|
+
with_temporary_keychain do |keychain_info|
|
51
|
+
archive_and_sign_hash = {
|
52
|
+
application_cert_name: keychain_info[:application_cert_name],
|
53
|
+
application_team_id: keychain_info[:application_team_id],
|
54
|
+
project_path: options.project_path,
|
55
|
+
workspace_path: options.workspace_path,
|
56
|
+
output_dir: options.output_dir,
|
57
|
+
skip_sparkle_steps: options.skip_sparkle_steps
|
58
|
+
}
|
37
59
|
|
38
|
-
|
39
|
-
Env.verbose_enabled = verbose
|
40
|
-
end
|
41
|
-
end.parse!
|
60
|
+
exported_app_path = Packaging.archive_and_sign(archive_and_sign_hash)
|
42
61
|
|
43
|
-
|
44
|
-
|
45
|
-
raise 'Xcode Project/Workspace path and Environment/Environment file are required.'
|
62
|
+
puts "Exported the app to #{exported_app_path}"
|
63
|
+
end
|
46
64
|
end
|
65
|
+
end
|
47
66
|
|
48
|
-
|
67
|
+
command :package do |c|
|
68
|
+
c.syntax = "#{AutoSparkle::NAME} package [options]"
|
69
|
+
c.description = 'Package the macOS app into a DMG file'
|
70
|
+
c.option('--app-path PATH', String, 'Path to the exported app')
|
71
|
+
c.option('--output-dir PATH', String, 'Path to the output directory')
|
72
|
+
c.action do |_args, options|
|
73
|
+
options.default \
|
74
|
+
output_dir: Dir.pwd
|
75
|
+
|
76
|
+
raise 'env name/path is required.' unless options.env
|
77
|
+
raise 'App path is required.' unless options.app_path
|
78
|
+
|
79
|
+
# Add the app-path file name without the extension to options
|
80
|
+
app_path = File.expand_path(options.app_path)
|
81
|
+
options.app_display_name = File.basename(app_path, '.*')
|
82
|
+
|
83
|
+
BuildDirectory.create_build_directory
|
84
|
+
Env.initialize(options, Command::PACKAGE)
|
85
|
+
|
86
|
+
with_temporary_keychain do |keychain_info|
|
87
|
+
dmg_path = Packaging.create_and_sign_dmg(
|
88
|
+
app_path,
|
89
|
+
keychain_info[:application_cert_name],
|
90
|
+
options.output_dir
|
91
|
+
)
|
92
|
+
|
93
|
+
puts "Packaged the app into a DMG file at #{dmg_path}"
|
94
|
+
end
|
95
|
+
end
|
49
96
|
end
|
50
97
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
98
|
+
command :distribute do |c|
|
99
|
+
c.syntax = "#{AutoSparkle::NAME} distribute [options]"
|
100
|
+
c.description = 'Distribute your package to the specified storage and update the appcast.xml file'
|
101
|
+
c.option('--dmg-path PATH', String, 'Path of the DMG file to be distributed')
|
102
|
+
c.option('--app-display-name NAME', String, 'Name of the app inside the DMG without the .app extension')
|
103
|
+
c.option('--marketing-version VERSION', String, 'Marketing version of the app')
|
104
|
+
c.option('--current-project-version VERSION', String, 'Current project version of the app')
|
105
|
+
c.option('--minimum-macos-version VERSION', String, 'Minimum macOS version required to run the app, defaults to 14.0')
|
106
|
+
c.action do |_args, options|
|
107
|
+
raise 'env name/path is required.' unless options.env
|
108
|
+
raise 'dmg path is required, please pass it using --dmg-path' unless options.dmg_path
|
109
|
+
raise 'No dmg file found at the specified path' unless File.exist?(options.dmg_path)
|
110
|
+
raise 'App display name is required.' unless options.app_display_name
|
111
|
+
|
112
|
+
options.default \
|
113
|
+
minimum_macos_version: '14.0'
|
114
|
+
|
115
|
+
BuildDirectory.create_build_directory
|
116
|
+
Env.initialize(options, Command::DISTRIBUTE)
|
117
|
+
|
118
|
+
Distribution.upload_update(options.dmg_path)
|
119
|
+
end
|
71
120
|
end
|
72
121
|
|
73
|
-
|
122
|
+
command :automate do |c|
|
123
|
+
c.syntax = "#{AutoSparkle::NAME} automate [options]"
|
124
|
+
c.description = 'Automate the export, packaging, and distribution of the macOS app'
|
125
|
+
c.option('--project-path PATH', String, 'Path to the Xcode project')
|
126
|
+
c.option('--workspace-path PATH', String, 'Path to the Xcode workspace')
|
127
|
+
c.action do |_args, options|
|
128
|
+
raise 'env name/path is required.' unless options.env
|
129
|
+
raise 'Xcode Project/Workspace path is required.' unless options.workspace_path || options.project_path
|
130
|
+
|
131
|
+
BuildDirectory.create_build_directory
|
132
|
+
Env.initialize(options, Command::AUTOMATE)
|
133
|
+
|
134
|
+
app_version = "#{Env.variables.marketing_version} (#{Env.variables.current_project_version})"
|
135
|
+
puts "Automating the delivery of #{Env.variables.app_display_name} version #{app_version} ..."
|
136
|
+
|
137
|
+
Xcodeproj.update_project_version(options.project_path, options.workspace_path)
|
138
|
+
|
139
|
+
dmg_path = nil
|
140
|
+
with_temporary_keychain do |keychain_info|
|
141
|
+
# 1. Export the app
|
142
|
+
archive_and_sign_hash = {
|
143
|
+
application_cert_name: keychain_info[:application_cert_name],
|
144
|
+
application_team_id: keychain_info[:application_team_id],
|
145
|
+
project_path: options.project_path,
|
146
|
+
workspace_path: options.workspace_path
|
147
|
+
}
|
148
|
+
|
149
|
+
exported_app_path = Packaging.archive_and_sign(archive_and_sign_hash)
|
150
|
+
|
151
|
+
# 2. Package the app
|
152
|
+
dmg_path = Packaging.create_and_sign_dmg(
|
153
|
+
exported_app_path,
|
154
|
+
keychain_info[:application_cert_name],
|
155
|
+
nil
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
# 3. Distribute the app
|
160
|
+
Distribution.upload_update(dmg_path)
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutoSparkle
|
4
|
+
NAME = 'autosparkle'
|
5
|
+
VERSION = '0.1.0'
|
6
|
+
SUMMARY = 'Automate macOS app delivery outside the App Store'
|
7
|
+
DESCRIPTION = 'Autosparkle is a command-line tool that automates the delivery of macOS apps outside the App Store. It is designed to be used in CI/CD pipelines to automate the process of archiving, signing, notarizing, and distributing macOS apps.'
|
8
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'Plist'
|
3
4
|
require_relative 'helpers/build_directory_helpers'
|
4
5
|
require_relative 'helpers/constants'
|
5
6
|
require_relative 'helpers/commands_helpers'
|
@@ -9,43 +10,50 @@ require_relative 'environment/environment'
|
|
9
10
|
|
10
11
|
# Packaging module to archive and sign the app
|
11
12
|
module Packaging
|
12
|
-
def self.archive_and_sign(
|
13
|
-
application_cert_name,
|
14
|
-
application_cert_team_id,
|
15
|
-
options
|
16
|
-
)
|
17
|
-
|
13
|
+
def self.archive_and_sign(hash)
|
18
14
|
puts_title 'Archiving and signing the app'
|
19
15
|
app_archive_path = BuildDirectory.new_path("#{Env.variables.app_display_name}.xcarchive")
|
20
16
|
archive_command = 'xcodebuild clean analyze archive'
|
21
17
|
archive_command += " -scheme #{Env.variables.scheme}"
|
22
18
|
archive_command += " -archivePath '#{app_archive_path}'"
|
23
|
-
archive_command += " CODE_SIGN_IDENTITY='#{application_cert_name}'"
|
24
|
-
archive_command += " DEVELOPMENT_TEAM='#{
|
19
|
+
archive_command += " CODE_SIGN_IDENTITY='#{hash[:application_cert_name]}'"
|
20
|
+
archive_command += " DEVELOPMENT_TEAM='#{hash[:application_team_id]}'"
|
25
21
|
archive_command += " -configuration #{Env.variables.configuration}"
|
26
22
|
archive_command += " OTHER_CODE_SIGN_FLAGS='--timestamp --options=runtime'"
|
27
23
|
|
28
|
-
archive_command += if
|
29
|
-
" -workspace #{
|
24
|
+
archive_command += if hash[:workspace_path]
|
25
|
+
" -workspace #{hash[:workspace_path]}"
|
30
26
|
else
|
31
|
-
" -project #{
|
27
|
+
" -project #{hash[:project_path]}"
|
32
28
|
end
|
33
29
|
|
34
30
|
execute_command(archive_command)
|
35
31
|
|
36
32
|
puts_title 'Exporting the app'
|
37
|
-
exported_app_path = export_app(
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
application_cert_name
|
33
|
+
exported_app_path = export_app(
|
34
|
+
app_archive_path,
|
35
|
+
hash[:application_cert_name],
|
36
|
+
hash[:application_team_id],
|
37
|
+
hash[:output_dir]
|
43
38
|
)
|
44
39
|
|
40
|
+
unless hash[:skip_sparkle_steps]
|
41
|
+
puts_title 'Signing the Sparkle framework'
|
42
|
+
sign_sparkle_framework(
|
43
|
+
exported_app_path,
|
44
|
+
hash[:application_cert_name]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
45
48
|
exported_app_path
|
46
49
|
end
|
47
50
|
|
48
|
-
def self.export_app(
|
51
|
+
def self.export_app(
|
52
|
+
app_archive_path,
|
53
|
+
application_cert_name,
|
54
|
+
team_id,
|
55
|
+
output_dir
|
56
|
+
)
|
49
57
|
puts_if_verbose 'Exporting the app...'
|
50
58
|
export_options = {
|
51
59
|
signingStyle: 'automatic',
|
@@ -71,43 +79,61 @@ module Packaging
|
|
71
79
|
export_command += " -exportOptionsPlist \"#{export_options_file.path}\""
|
72
80
|
execute_command(export_command)
|
73
81
|
|
74
|
-
"#{export_app_dir_path}/#{Env.variables.app_display_name}.app"
|
82
|
+
exported_app_path = "#{export_app_dir_path}/#{Env.variables.app_display_name}.app"
|
83
|
+
return exported_app_path unless output_dir
|
84
|
+
|
85
|
+
FileUtils.cp_r(exported_app_path, output_dir)
|
86
|
+
"#{output_dir}/#{Env.variables.app_display_name}.app"
|
75
87
|
end
|
76
88
|
|
77
|
-
def self.create_and_sign_dmg(
|
89
|
+
def self.create_and_sign_dmg(
|
90
|
+
exported_app_path,
|
91
|
+
application_cert_name,
|
92
|
+
output_dir
|
93
|
+
)
|
78
94
|
puts_title "Creating #{Env.variables.app_display_name}.dmg"
|
79
95
|
puts_if_verbose 'Creating a DMG for the app...'
|
80
96
|
dmg_path = DMG.create(exported_app_path)
|
81
97
|
|
82
98
|
# Sign the DMG
|
83
99
|
puts_if_verbose 'Signing the DMG...'
|
84
|
-
|
100
|
+
signing_dmg_command = "codesign --force --sign \"#{application_cert_name}\""
|
101
|
+
signing_dmg_command += " --timestamp --options runtime \"#{dmg_path}\""
|
102
|
+
execute_command(signing_dmg_command)
|
85
103
|
|
86
104
|
# Notarize the DMG
|
87
105
|
puts_if_verbose 'Notarizing the DMG...'
|
88
|
-
|
106
|
+
notarize_command = "xcrun notarytool submit \"#{dmg_path}\""
|
107
|
+
notarize_command += " --keychain-profile \"#{Constants::NOTARIZE_KEYCHAIN_PROFILE}\""
|
108
|
+
notarize_command += " --keychain \"#{Constants::KEYCHAIN_PATH}\""
|
109
|
+
notarize_command += ' --wait'
|
110
|
+
execute_command(notarize_command)
|
89
111
|
|
90
112
|
# Staple the notarization ticket
|
91
113
|
puts_if_verbose 'Stapling the notarization ticket...'
|
92
114
|
execute_command("xcrun stapler staple \"#{dmg_path}\"")
|
93
115
|
|
94
|
-
dmg_path
|
116
|
+
return dmg_path unless output_dir
|
117
|
+
|
118
|
+
FileUtils.cp(dmg_path, output_dir)
|
119
|
+
"#{output_dir}/#{Env.variables.app_display_name}.dmg"
|
95
120
|
end
|
96
121
|
|
97
122
|
def self.sign_sparkle_framework(exported_app_path, application_cert_name)
|
98
|
-
|
123
|
+
sparkle_framework_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework"
|
124
|
+
|
125
|
+
sparkle_auto_update_path = "#{sparkle_framework_path}/AutoUpdate"
|
99
126
|
codesign('Signing Sparkle AutoUpdate...', application_cert_name, sparkle_auto_update_path)
|
100
127
|
|
101
|
-
sparkle_updater_path = "#{
|
128
|
+
sparkle_updater_path = "#{sparkle_framework_path}/Updater.app"
|
102
129
|
codesign('Signing Sparkle Updater...', application_cert_name, sparkle_updater_path)
|
103
130
|
|
104
|
-
sparkle_installer_xpc_path = "#{
|
131
|
+
sparkle_installer_xpc_path = "#{sparkle_framework_path}/XPCServices/Installer.xpc/Contents/MacOS/Installer"
|
105
132
|
codesign('Signing Sparkle Installer XPC Service...', application_cert_name, sparkle_installer_xpc_path)
|
106
133
|
|
107
|
-
sparkle_downloader_xpc_path = "#{
|
134
|
+
sparkle_downloader_xpc_path = "#{sparkle_framework_path}/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
|
108
135
|
codesign('Signing Sparkle Downloader XPC Service...', application_cert_name, sparkle_downloader_xpc_path)
|
109
136
|
|
110
|
-
sparkle_framework_path = "#{exported_app_path}/Contents/Frameworks/Sparkle.framework"
|
111
137
|
codesign('Signing Sparkle framework...', application_cert_name, sparkle_framework_path)
|
112
138
|
end
|
113
139
|
|
Binary file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: autosparkle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hadi Dbouk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-s3
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 1.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: commander
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 5.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 5.0.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: dotenv
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,7 +121,9 @@ files:
|
|
107
121
|
- lib/autosparkle/helpers/version_helpers.rb
|
108
122
|
- lib/autosparkle/helpers/xcodeproj_helpers.rb
|
109
123
|
- lib/autosparkle/main.rb
|
124
|
+
- lib/autosparkle/metadata.rb
|
110
125
|
- lib/autosparkle/packaging.rb
|
126
|
+
- lib/autosparkle/resources/default-dmg-background.png
|
111
127
|
- lib/autosparkle/sparkle/BinaryDelta
|
112
128
|
- lib/autosparkle/sparkle/generate_appcast
|
113
129
|
- lib/autosparkle/sparkle/generate_keys
|