mysigner 0.1.1 → 0.1.3
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 +4 -4
- data/.githooks/pre-commit +15 -0
- data/.githooks/pre-push +21 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +112 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +38 -8
- data/README.md +87 -17
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/setup +3 -0
- data/exe/mysigner +2 -1
- data/lib/mysigner/build/android_executor.rb +46 -52
- data/lib/mysigner/build/android_parser.rb +33 -40
- data/lib/mysigner/build/configurator.rb +17 -16
- data/lib/mysigner/build/detector.rb +39 -50
- data/lib/mysigner/build/error_analyzer.rb +70 -68
- data/lib/mysigner/build/executor.rb +30 -37
- data/lib/mysigner/build/parser.rb +18 -18
- data/lib/mysigner/cli/auth_commands.rb +735 -752
- data/lib/mysigner/cli/build_commands.rb +697 -721
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
- data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
- data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
- data/lib/mysigner/cli/concerns/helpers.rb +12 -1
- data/lib/mysigner/cli/diagnostic_commands.rb +659 -635
- data/lib/mysigner/cli/resource_commands.rb +1266 -822
- data/lib/mysigner/cli/validate_commands.rb +161 -0
- data/lib/mysigner/cli.rb +5 -1
- data/lib/mysigner/client.rb +27 -19
- data/lib/mysigner/config.rb +93 -56
- data/lib/mysigner/export/exporter.rb +32 -36
- data/lib/mysigner/signing/certificate_checker.rb +18 -23
- data/lib/mysigner/signing/keystore_manager.rb +34 -39
- data/lib/mysigner/signing/validator.rb +38 -40
- data/lib/mysigner/signing/wizard.rb +329 -342
- data/lib/mysigner/upload/app_store_automation.rb +51 -49
- data/lib/mysigner/upload/app_store_submission.rb +87 -92
- data/lib/mysigner/upload/play_store_uploader.rb +98 -115
- data/lib/mysigner/upload/uploader.rb +101 -109
- data/lib/mysigner/version.rb +3 -1
- data/lib/mysigner.rb +13 -11
- data/mysigner.gemspec +36 -33
- data/test_manual.rb +37 -36
- metadata +38 -16
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
1
4
|
require 'fileutils'
|
|
2
5
|
require 'tmpdir'
|
|
3
6
|
|
|
@@ -9,31 +12,27 @@ module Mysigner
|
|
|
9
12
|
def initialize(archive_path, output_dir: nil)
|
|
10
13
|
@archive_path = File.expand_path(archive_path)
|
|
11
14
|
@output_dir = output_dir || File.dirname(@archive_path)
|
|
12
|
-
|
|
15
|
+
|
|
13
16
|
validate_archive!
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def export!(method: :appstore, team_id: nil, signing_style: 'automatic')
|
|
17
20
|
say_exporting(method)
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
# Generate export options plist
|
|
20
23
|
options_plist = generate_export_options(method, team_id, signing_style)
|
|
21
|
-
|
|
24
|
+
|
|
22
25
|
begin
|
|
23
26
|
# Run xcodebuild -exportArchive
|
|
24
27
|
success = execute_export(options_plist)
|
|
25
|
-
|
|
26
|
-
unless success
|
|
27
|
-
|
|
28
|
-
end
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
raise ExportError, 'Export failed. Check output above for errors.' unless success
|
|
30
|
+
|
|
30
31
|
# Find the generated .ipa file
|
|
31
32
|
ipa_path = find_ipa_file
|
|
32
|
-
|
|
33
|
-
unless ipa_path
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
|
|
33
|
+
|
|
34
|
+
raise ExportError, "Export reported success but .ipa file not found in: #{@output_dir}" unless ipa_path
|
|
35
|
+
|
|
37
36
|
ipa_path
|
|
38
37
|
ensure
|
|
39
38
|
# Clean up temp plist
|
|
@@ -44,42 +43,40 @@ module Mysigner
|
|
|
44
43
|
private
|
|
45
44
|
|
|
46
45
|
def validate_archive!
|
|
47
|
-
unless File.exist?(@archive_path)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
raise ExportError, "Invalid archive: #{@archive_path} (must be a .xcarchive directory)"
|
|
53
|
-
end
|
|
46
|
+
raise ExportError, "Archive not found: #{@archive_path}" unless File.exist?(@archive_path)
|
|
47
|
+
|
|
48
|
+
return if File.directory?(@archive_path) && @archive_path.end_with?('.xcarchive')
|
|
49
|
+
|
|
50
|
+
raise ExportError, "Invalid archive: #{@archive_path} (must be a .xcarchive directory)"
|
|
54
51
|
end
|
|
55
52
|
|
|
56
53
|
def generate_export_options(method, team_id, signing_style)
|
|
57
54
|
require 'plist'
|
|
58
|
-
|
|
55
|
+
|
|
59
56
|
options = {
|
|
60
57
|
'method' => export_method_string(method),
|
|
61
58
|
'uploadBitcode' => false,
|
|
62
59
|
'uploadSymbols' => false,
|
|
63
60
|
'compileBitcode' => false
|
|
64
61
|
}
|
|
65
|
-
|
|
62
|
+
|
|
66
63
|
# Add team ID if provided
|
|
67
64
|
options['teamID'] = team_id if team_id
|
|
68
|
-
|
|
65
|
+
|
|
69
66
|
# Signing style
|
|
70
67
|
if signing_style.to_s.downcase == 'manual'
|
|
71
68
|
options['signingStyle'] = 'manual'
|
|
72
|
-
#
|
|
69
|
+
# NOTE: For manual signing, we'd need to specify provisioningProfiles
|
|
73
70
|
# But since the archive was already signed during build, we can often omit this
|
|
74
71
|
else
|
|
75
72
|
options['signingStyle'] = 'automatic'
|
|
76
73
|
options['signingCertificate'] = 'Apple Distribution'
|
|
77
74
|
end
|
|
78
|
-
|
|
75
|
+
|
|
79
76
|
# Create temp plist file
|
|
80
77
|
plist_path = File.join(Dir.tmpdir, "exportOptions-#{Time.now.to_i}.plist")
|
|
81
78
|
File.write(plist_path, options.to_plist)
|
|
82
|
-
|
|
79
|
+
|
|
83
80
|
plist_path
|
|
84
81
|
end
|
|
85
82
|
|
|
@@ -100,7 +97,7 @@ module Mysigner
|
|
|
100
97
|
|
|
101
98
|
def execute_export(options_plist)
|
|
102
99
|
FileUtils.mkdir_p(@output_dir)
|
|
103
|
-
puts
|
|
100
|
+
puts ''
|
|
104
101
|
|
|
105
102
|
cmd = [
|
|
106
103
|
'xcodebuild',
|
|
@@ -112,39 +109,38 @@ module Mysigner
|
|
|
112
109
|
].join(' ')
|
|
113
110
|
|
|
114
111
|
# Run command and capture output
|
|
115
|
-
IO.popen(cmd, err: [
|
|
112
|
+
IO.popen(cmd, err: %i[child out]) do |io|
|
|
116
113
|
io.each_line do |line|
|
|
117
114
|
next if line.strip.empty?
|
|
118
|
-
|
|
115
|
+
|
|
119
116
|
# Show errors and warnings
|
|
120
117
|
if line.include?('error:') || line.include?('warning:')
|
|
121
118
|
puts line
|
|
122
119
|
# Show progress markers
|
|
123
|
-
elsif line.include?('Exporting') || line.include?('Processing') ||
|
|
120
|
+
elsif line.include?('Exporting') || line.include?('Processing') ||
|
|
124
121
|
line.include?('Validating')
|
|
125
122
|
print '.'
|
|
126
123
|
end
|
|
127
124
|
end
|
|
128
125
|
end
|
|
129
126
|
|
|
130
|
-
puts
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
puts '' # New line after dots
|
|
128
|
+
|
|
129
|
+
$CHILD_STATUS.success?
|
|
133
130
|
end
|
|
134
131
|
|
|
135
132
|
def find_ipa_file
|
|
136
133
|
# Look for .ipa files in output directory
|
|
137
134
|
ipa_files = Dir.glob(File.join(@output_dir, '*.ipa'))
|
|
138
|
-
|
|
135
|
+
|
|
139
136
|
# Return the most recently created one
|
|
140
137
|
ipa_files.max_by { |f| File.mtime(f) }
|
|
141
138
|
end
|
|
142
139
|
|
|
143
140
|
def say_exporting(method)
|
|
144
141
|
puts "📦 Exporting archive for #{method}..."
|
|
145
|
-
puts
|
|
142
|
+
puts ''
|
|
146
143
|
end
|
|
147
144
|
end
|
|
148
145
|
end
|
|
149
146
|
end
|
|
150
|
-
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'open3'
|
|
2
4
|
require 'time'
|
|
3
5
|
|
|
@@ -42,23 +44,21 @@ module Mysigner
|
|
|
42
44
|
cmd = 'security find-identity -v -p codesigning'
|
|
43
45
|
stdout, stderr, status = Open3.capture3(cmd)
|
|
44
46
|
|
|
45
|
-
unless status.success?
|
|
46
|
-
raise CheckError, "Failed to query certificates: #{stderr}"
|
|
47
|
-
end
|
|
47
|
+
raise CheckError, "Failed to query certificates: #{stderr}" unless status.success?
|
|
48
48
|
|
|
49
49
|
certificates = []
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
# Parse output: " 1) HASH \"Certificate Name\""
|
|
52
52
|
stdout.each_line do |line|
|
|
53
53
|
next unless line =~ /\d+\)\s+([A-F0-9]+)\s+"([^"]+)"/
|
|
54
|
-
|
|
55
|
-
hash =
|
|
56
|
-
name =
|
|
57
|
-
|
|
54
|
+
|
|
55
|
+
hash = ::Regexp.last_match(1)
|
|
56
|
+
name = ::Regexp.last_match(2)
|
|
57
|
+
|
|
58
58
|
# Get certificate details
|
|
59
59
|
details = get_certificate_details(name)
|
|
60
60
|
next unless details
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
certificates << {
|
|
63
63
|
hash: hash,
|
|
64
64
|
name: name,
|
|
@@ -76,12 +76,10 @@ module Mysigner
|
|
|
76
76
|
def get_certificate_details(name)
|
|
77
77
|
# Get certificate in PEM format by name
|
|
78
78
|
cmd = "security find-certificate -c \"#{name}\" -p"
|
|
79
|
-
stdout,
|
|
79
|
+
stdout, _, status = Open3.capture3(cmd)
|
|
80
80
|
|
|
81
81
|
# If not found, return nil
|
|
82
|
-
if !status.success? || stdout.empty?
|
|
83
|
-
return nil
|
|
84
|
-
end
|
|
82
|
+
return nil if !status.success? || stdout.empty?
|
|
85
83
|
|
|
86
84
|
# Save to temp file and use openssl to read expiry
|
|
87
85
|
require 'tempfile'
|
|
@@ -92,12 +90,12 @@ module Mysigner
|
|
|
92
90
|
|
|
93
91
|
# Get expiry date using openssl
|
|
94
92
|
cmd = "openssl x509 -in #{temp.path} -noout -enddate"
|
|
95
|
-
out,
|
|
93
|
+
out, _, stat = Open3.capture3(cmd)
|
|
96
94
|
|
|
97
95
|
if stat.success? && out =~ /notAfter=(.+)/
|
|
98
|
-
expiry_str =
|
|
96
|
+
expiry_str = ::Regexp.last_match(1).strip
|
|
99
97
|
expires_at = Time.parse(expiry_str)
|
|
100
|
-
days_until_expiry = ((expires_at - Time.now) /
|
|
98
|
+
days_until_expiry = ((expires_at - Time.now) / 86_400).to_i
|
|
101
99
|
|
|
102
100
|
return {
|
|
103
101
|
expires_at: expires_at,
|
|
@@ -113,11 +111,9 @@ module Mysigner
|
|
|
113
111
|
|
|
114
112
|
def extract_team_id(name)
|
|
115
113
|
# Team ID is usually in parentheses at the end
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
114
|
+
return unless name =~ /\(([A-Z0-9]{10})\)$/
|
|
115
|
+
|
|
116
|
+
::Regexp.last_match(1)
|
|
121
117
|
end
|
|
122
118
|
|
|
123
119
|
def determine_cert_type(name)
|
|
@@ -134,7 +130,7 @@ module Mysigner
|
|
|
134
130
|
end
|
|
135
131
|
|
|
136
132
|
def determine_status(days_until_expiry)
|
|
137
|
-
if days_until_expiry
|
|
133
|
+
if days_until_expiry.negative?
|
|
138
134
|
STATUS_EXPIRED
|
|
139
135
|
elsif days_until_expiry < 30
|
|
140
136
|
STATUS_EXPIRING_SOON
|
|
@@ -145,4 +141,3 @@ module Mysigner
|
|
|
145
141
|
end
|
|
146
142
|
end
|
|
147
143
|
end
|
|
148
|
-
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
1
4
|
require 'fileutils'
|
|
2
5
|
require 'base64'
|
|
3
6
|
|
|
@@ -44,21 +47,19 @@ module Mysigner
|
|
|
44
47
|
# Get keystore details
|
|
45
48
|
keystores = list
|
|
46
49
|
keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
|
|
47
|
-
|
|
48
|
-
unless keystore
|
|
49
|
-
raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found"
|
|
50
|
-
end
|
|
50
|
+
|
|
51
|
+
raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found" unless keystore
|
|
51
52
|
|
|
52
53
|
# Download the keystore file
|
|
53
54
|
download_url = "/api/v1/organizations/#{@organization_id}/android_keystores/#{keystore_id}/download"
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
conn = build_download_connection
|
|
56
57
|
response = conn.get(download_url)
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
unless response.success?
|
|
59
60
|
error_msg = begin
|
|
60
61
|
JSON.parse(response.body)['message']
|
|
61
|
-
rescue
|
|
62
|
+
rescue StandardError
|
|
62
63
|
"HTTP #{response.status}"
|
|
63
64
|
end
|
|
64
65
|
raise DownloadError, "Failed to download keystore: #{error_msg}"
|
|
@@ -67,9 +68,9 @@ module Mysigner
|
|
|
67
68
|
# Save to local file
|
|
68
69
|
filename = "#{keystore['name'].gsub(/[^a-zA-Z0-9_.-]/, '_')}.jks"
|
|
69
70
|
local_path = File.join(KEYSTORES_DIR, filename)
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
File.binwrite(local_path, response.body)
|
|
72
|
-
File.chmod(
|
|
73
|
+
File.chmod(0o600, local_path) # Secure permissions
|
|
73
74
|
|
|
74
75
|
{
|
|
75
76
|
path: local_path,
|
|
@@ -83,10 +84,8 @@ module Mysigner
|
|
|
83
84
|
def get_or_download(keystore_id)
|
|
84
85
|
keystores = list
|
|
85
86
|
keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
|
|
86
|
-
|
|
87
|
-
unless keystore
|
|
88
|
-
raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found"
|
|
89
|
-
end
|
|
87
|
+
|
|
88
|
+
raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found" unless keystore
|
|
90
89
|
|
|
91
90
|
# Check if already cached locally
|
|
92
91
|
filename = "#{keystore['name'].gsub(/[^a-zA-Z0-9_.-]/, '_')}.jks"
|
|
@@ -109,10 +108,9 @@ module Mysigner
|
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
# Upload a keystore to API
|
|
112
|
-
def upload(name:, keystore_path:, keystore_password:, key_alias:, key_password: nil, android_app_id: nil,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end
|
|
111
|
+
def upload(name:, keystore_path:, keystore_password:, key_alias:, key_password: nil, android_app_id: nil,
|
|
112
|
+
active: true)
|
|
113
|
+
raise KeystoreError, "Keystore file not found: #{keystore_path}" unless File.exist?(keystore_path)
|
|
116
114
|
|
|
117
115
|
# Read and encode keystore
|
|
118
116
|
keystore_content = File.binread(keystore_path)
|
|
@@ -141,14 +139,18 @@ module Mysigner
|
|
|
141
139
|
# Delete a keystore
|
|
142
140
|
def delete(keystore_id)
|
|
143
141
|
@client.delete("/api/v1/organizations/#{@organization_id}/android_keystores/#{keystore_id}")
|
|
144
|
-
|
|
142
|
+
|
|
145
143
|
# Also remove local cached file
|
|
146
|
-
keystores =
|
|
144
|
+
keystores = begin
|
|
145
|
+
list
|
|
146
|
+
rescue StandardError
|
|
147
|
+
[]
|
|
148
|
+
end
|
|
147
149
|
keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
|
|
148
150
|
if keystore
|
|
149
151
|
filename = "#{keystore['name'].gsub(/[^a-zA-Z0-9_.-]/, '_')}.jks"
|
|
150
152
|
local_path = File.join(KEYSTORES_DIR, filename)
|
|
151
|
-
|
|
153
|
+
FileUtils.rm_f(local_path)
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
true
|
|
@@ -187,26 +189,18 @@ module Mysigner
|
|
|
187
189
|
return nil unless system('which keytool > /dev/null 2>&1')
|
|
188
190
|
|
|
189
191
|
output = `keytool -list -v -keystore #{shell_escape(keystore_path)} -storepass #{shell_escape(password)} 2>&1`
|
|
190
|
-
return nil unless
|
|
192
|
+
return nil unless $CHILD_STATUS.success?
|
|
191
193
|
|
|
192
194
|
# Parse output
|
|
193
195
|
info = {}
|
|
194
|
-
|
|
195
|
-
if output =~ /Alias name: (.+)/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if output =~ /
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if output =~ /SHA256: (.+)/
|
|
204
|
-
info[:sha256] = $1.strip
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
if output =~ /SHA1: (.+)/
|
|
208
|
-
info[:sha1] = $1.strip
|
|
209
|
-
end
|
|
196
|
+
|
|
197
|
+
info[:aliases] = output.scan(/Alias name: (.+)/).flatten if output =~ /Alias name: (.+)/
|
|
198
|
+
|
|
199
|
+
info[:expires] = ::Regexp.last_match(1) if output =~ /Valid from: .+ until: (.+)/
|
|
200
|
+
|
|
201
|
+
info[:sha256] = ::Regexp.last_match(1).strip if output =~ /SHA256: (.+)/
|
|
202
|
+
|
|
203
|
+
info[:sha1] = ::Regexp.last_match(1).strip if output =~ /SHA1: (.+)/
|
|
210
204
|
|
|
211
205
|
info
|
|
212
206
|
end
|
|
@@ -215,7 +209,7 @@ module Mysigner
|
|
|
215
209
|
|
|
216
210
|
def ensure_keystores_dir
|
|
217
211
|
FileUtils.mkdir_p(KEYSTORES_DIR)
|
|
218
|
-
File.chmod(
|
|
212
|
+
File.chmod(0o700, KEYSTORES_DIR) # Secure permissions
|
|
219
213
|
end
|
|
220
214
|
|
|
221
215
|
def build_download_connection
|
|
@@ -232,7 +226,8 @@ module Mysigner
|
|
|
232
226
|
|
|
233
227
|
def shell_escape(str)
|
|
234
228
|
return "''" if str.nil? || str.empty?
|
|
235
|
-
|
|
229
|
+
|
|
230
|
+
"'#{str.gsub("'", "'\\''")}'"
|
|
236
231
|
end
|
|
237
232
|
end
|
|
238
233
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mysigner
|
|
2
4
|
module Signing
|
|
3
5
|
class Validator
|
|
@@ -19,20 +21,20 @@ module Mysigner
|
|
|
19
21
|
team_id = @team_id_override || @parser.team_id(@target_name, @configuration)
|
|
20
22
|
if team_id.nil? || team_id.empty?
|
|
21
23
|
result[:errors] << "No development team set for target '#{@target_name}'"
|
|
22
|
-
result[:errors] <<
|
|
23
|
-
result[:errors] <<
|
|
24
|
-
result[:errors] <<
|
|
25
|
-
result[:errors] <<
|
|
26
|
-
result[:errors] <<
|
|
27
|
-
result[:errors] <<
|
|
28
|
-
result[:errors] <<
|
|
29
|
-
result[:errors] <<
|
|
30
|
-
result[:errors] <<
|
|
31
|
-
result[:errors] <<
|
|
32
|
-
result[:errors] <<
|
|
33
|
-
result[:errors] <<
|
|
34
|
-
result[:errors] <<
|
|
35
|
-
result[:errors] <<
|
|
24
|
+
result[:errors] << ''
|
|
25
|
+
result[:errors] << 'Fix Option 1: Add team to My Signer'
|
|
26
|
+
result[:errors] << ' 1. Open https://mysigner.dev'
|
|
27
|
+
result[:errors] << ' 2. Go to Settings → App Store Connect'
|
|
28
|
+
result[:errors] << ' 3. Add your Team ID'
|
|
29
|
+
result[:errors] << ' 4. Run: mysigner build (team will auto-fetch)'
|
|
30
|
+
result[:errors] << ''
|
|
31
|
+
result[:errors] << 'Fix Option 2: Pass team via CLI'
|
|
32
|
+
result[:errors] << ' mysigner build --team YOUR_TEAM_ID'
|
|
33
|
+
result[:errors] << ''
|
|
34
|
+
result[:errors] << 'Fix Option 3: Set in Xcode'
|
|
35
|
+
result[:errors] << ' Open Xcode → Select target → Signing & Capabilities → Select a team'
|
|
36
|
+
result[:errors] << ''
|
|
37
|
+
result[:errors] << 'Find your team ID at: https://developer.apple.com/account/#!/membership/'
|
|
36
38
|
result[:valid] = false
|
|
37
39
|
elsif @team_id_override && @parser.team_id(@target_name, @configuration).nil?
|
|
38
40
|
result[:warnings] << "Using team from My Signer: #{@team_id_override}"
|
|
@@ -42,15 +44,13 @@ module Mysigner
|
|
|
42
44
|
|
|
43
45
|
# Check 2: Code signing style
|
|
44
46
|
signing_style = @parser.code_sign_style(@target_name, @configuration)
|
|
45
|
-
if signing_style.nil?
|
|
46
|
-
result[:warnings] << "Code signing style not explicitly set (will use Xcode default)"
|
|
47
|
-
end
|
|
47
|
+
result[:warnings] << 'Code signing style not explicitly set (will use Xcode default)' if signing_style.nil?
|
|
48
48
|
|
|
49
49
|
# Check 3: Bundle ID
|
|
50
50
|
bundle_id = @parser.bundle_id(@target_name, @configuration)
|
|
51
51
|
if bundle_id.nil? || bundle_id.empty? || bundle_id.include?('$(')
|
|
52
52
|
result[:errors] << "Bundle ID not set or contains variables: #{bundle_id}"
|
|
53
|
-
result[:errors] <<
|
|
53
|
+
result[:errors] << 'Fix: Open Xcode → Select target → General → Bundle Identifier'
|
|
54
54
|
result[:valid] = false
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -79,22 +79,22 @@ module Mysigner
|
|
|
79
79
|
result = validate
|
|
80
80
|
|
|
81
81
|
if result[:warnings].any?
|
|
82
|
-
puts
|
|
83
|
-
puts
|
|
82
|
+
puts ''
|
|
83
|
+
puts '⚠️ Warnings:'
|
|
84
84
|
result[:warnings].each { |w| puts " • #{w}" }
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
if result[:errors].any?
|
|
88
|
-
puts
|
|
89
|
-
puts
|
|
90
|
-
puts
|
|
88
|
+
puts ''
|
|
89
|
+
puts '❌ Validation Failed:'
|
|
90
|
+
puts ''
|
|
91
91
|
result[:errors].each { |e| puts " • #{e}" }
|
|
92
|
-
puts
|
|
93
|
-
raise ValidationError,
|
|
92
|
+
puts ''
|
|
93
|
+
raise ValidationError, 'Pre-build validation failed. Fix the errors above and try again.'
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
puts
|
|
97
|
-
puts
|
|
96
|
+
puts '✓ Pre-build validation passed' if result[:warnings].empty?
|
|
97
|
+
puts '' if result[:warnings].any?
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
private
|
|
@@ -104,19 +104,17 @@ module Mysigner
|
|
|
104
104
|
|
|
105
105
|
# Check for valid iOS distribution certificates in keychain
|
|
106
106
|
output = `security find-identity -v -p codesigning 2>&1`
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
if output.include?('0 valid identities found')
|
|
109
|
-
result[:errors] <<
|
|
110
|
-
result[:errors] <<
|
|
109
|
+
result[:errors] << 'No code signing certificates found in keychain'
|
|
110
|
+
result[:errors] << 'Fix: Install your distribution certificate (.p12 file)'
|
|
111
111
|
else
|
|
112
112
|
# Count valid certificates
|
|
113
113
|
cert_count = output.scan(/\d+\)\s+[A-F0-9]+/).count
|
|
114
114
|
result[:warnings] << "Found #{cert_count} code signing certificate(s) in keychain"
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
# Check for expired certificates
|
|
117
|
-
if output.include?('CSSMERR')
|
|
118
|
-
result[:warnings] << "Some certificates may be invalid or expired"
|
|
119
|
-
end
|
|
117
|
+
result[:warnings] << 'Some certificates may be invalid or expired' if output.include?('CSSMERR')
|
|
120
118
|
end
|
|
121
119
|
|
|
122
120
|
result
|
|
@@ -127,18 +125,18 @@ module Mysigner
|
|
|
127
125
|
|
|
128
126
|
# Check provisioning profile directory
|
|
129
127
|
profiles_dir = File.expand_path('~/Library/MobileDevice/Provisioning Profiles')
|
|
130
|
-
|
|
128
|
+
|
|
131
129
|
unless Dir.exist?(profiles_dir)
|
|
132
|
-
result[:warnings] <<
|
|
133
|
-
result[:warnings] <<
|
|
130
|
+
result[:warnings] << 'No provisioning profiles directory found'
|
|
131
|
+
result[:warnings] << 'Profiles will be downloaded automatically if using automatic signing'
|
|
134
132
|
return result
|
|
135
133
|
end
|
|
136
134
|
|
|
137
135
|
profiles = Dir.glob("#{profiles_dir}/*.mobileprovision")
|
|
138
|
-
|
|
136
|
+
|
|
139
137
|
if profiles.empty?
|
|
140
|
-
result[:warnings] <<
|
|
141
|
-
result[:warnings] <<
|
|
138
|
+
result[:warnings] << 'No provisioning profiles found'
|
|
139
|
+
result[:warnings] << 'Profiles will be downloaded automatically if using automatic signing'
|
|
142
140
|
else
|
|
143
141
|
result[:warnings] << "Found #{profiles.count} provisioning profile(s) installed"
|
|
144
142
|
end
|