mysigner 0.1.0
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 +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +137 -0
- data/LICENSE +201 -0
- data/MANUAL_TEST.md +341 -0
- data/README.md +493 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/mysigner +5 -0
- data/lib/mysigner/build/android_executor.rb +367 -0
- data/lib/mysigner/build/android_parser.rb +293 -0
- data/lib/mysigner/build/configurator.rb +126 -0
- data/lib/mysigner/build/detector.rb +388 -0
- data/lib/mysigner/build/error_analyzer.rb +193 -0
- data/lib/mysigner/build/executor.rb +176 -0
- data/lib/mysigner/build/parser.rb +206 -0
- data/lib/mysigner/cli/auth_commands.rb +1381 -0
- data/lib/mysigner/cli/build_commands.rb +2095 -0
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +500 -0
- data/lib/mysigner/cli/concerns/api_helpers.rb +131 -0
- data/lib/mysigner/cli/concerns/error_handlers.rb +446 -0
- data/lib/mysigner/cli/concerns/helpers.rb +63 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +1034 -0
- data/lib/mysigner/cli/resource_commands.rb +2670 -0
- data/lib/mysigner/cli.rb +43 -0
- data/lib/mysigner/client.rb +189 -0
- data/lib/mysigner/config.rb +311 -0
- data/lib/mysigner/export/exporter.rb +150 -0
- data/lib/mysigner/signing/certificate_checker.rb +148 -0
- data/lib/mysigner/signing/keystore_manager.rb +239 -0
- data/lib/mysigner/signing/validator.rb +150 -0
- data/lib/mysigner/signing/wizard.rb +784 -0
- data/lib/mysigner/upload/app_store_automation.rb +402 -0
- data/lib/mysigner/upload/app_store_submission.rb +312 -0
- data/lib/mysigner/upload/play_store_uploader.rb +378 -0
- data/lib/mysigner/upload/uploader.rb +373 -0
- data/lib/mysigner/version.rb +3 -0
- data/lib/mysigner.rb +15 -0
- data/mysigner.gemspec +78 -0
- data/test_manual.rb +102 -0
- metadata +286 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
module Mysigner
|
|
2
|
+
module Build
|
|
3
|
+
class ErrorAnalyzer
|
|
4
|
+
attr_reader :issues
|
|
5
|
+
|
|
6
|
+
def initialize(build_errors)
|
|
7
|
+
@build_errors = build_errors || []
|
|
8
|
+
@issues = []
|
|
9
|
+
analyze!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def any_issues?
|
|
13
|
+
@issues.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns formatted suggestions for CLI output
|
|
17
|
+
def format_suggestions
|
|
18
|
+
return nil unless any_issues?
|
|
19
|
+
|
|
20
|
+
lines = []
|
|
21
|
+
lines << ""
|
|
22
|
+
lines << "=" * 70
|
|
23
|
+
lines << " 💡 SUGGESTIONS: How to fix these build errors"
|
|
24
|
+
lines << "=" * 70
|
|
25
|
+
lines << ""
|
|
26
|
+
|
|
27
|
+
# Group issues by type for cleaner output
|
|
28
|
+
profile_issues = @issues.select { |i| i[:type] == :profile_capability }
|
|
29
|
+
cert_issues = @issues.select { |i| i[:type] == :certificate_mismatch }
|
|
30
|
+
identifier_issues = @issues.select { |i| i[:type] == :missing_identifier }
|
|
31
|
+
|
|
32
|
+
# Profile capability issues
|
|
33
|
+
if profile_issues.any?
|
|
34
|
+
lines << " 📋 PROVISIONING PROFILE ISSUES"
|
|
35
|
+
lines << ""
|
|
36
|
+
|
|
37
|
+
# Group by profile name
|
|
38
|
+
by_profile = profile_issues.group_by { |i| i[:profile_name] }
|
|
39
|
+
by_profile.each do |profile_name, issues|
|
|
40
|
+
capabilities = issues.map { |i| i[:capability] }.compact.uniq
|
|
41
|
+
lines << " Profile: \"#{profile_name}\""
|
|
42
|
+
lines << " Missing capabilities: #{capabilities.join(', ')}"
|
|
43
|
+
lines << ""
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
lines << " How to fix:"
|
|
47
|
+
lines << " 1. Go to Apple Developer Portal → Certificates, Identifiers & Profiles"
|
|
48
|
+
lines << " 2. Select 'Identifiers' and find your Bundle ID"
|
|
49
|
+
lines << " 3. Enable the missing capabilities (App Groups, Apple Pay, etc.)"
|
|
50
|
+
lines << " 4. If adding App Groups or Merchant IDs, make sure to select the specific identifiers"
|
|
51
|
+
lines << " 5. Go to 'Profiles' and regenerate the affected provisioning profiles"
|
|
52
|
+
lines << " 6. Download new profiles: mysigner sync ios && mysigner profile download <ID>"
|
|
53
|
+
lines << " 7. Install profiles to: ~/Library/MobileDevice/Provisioning Profiles/"
|
|
54
|
+
lines << ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Missing specific identifiers (App Group ID, Merchant ID)
|
|
58
|
+
if identifier_issues.any?
|
|
59
|
+
lines << " 🔗 MISSING IDENTIFIERS"
|
|
60
|
+
lines << ""
|
|
61
|
+
|
|
62
|
+
identifier_issues.each do |issue|
|
|
63
|
+
lines << " Profile: \"#{issue[:profile_name]}\""
|
|
64
|
+
lines << " Missing: #{issue[:identifier_type]} - #{issue[:identifier]}"
|
|
65
|
+
lines << ""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
lines << " How to fix:"
|
|
69
|
+
lines << " 1. Go to Apple Developer Portal → Identifiers"
|
|
70
|
+
lines << " 2. Find your Bundle ID and edit it"
|
|
71
|
+
lines << " 3. Under the capability, add/select the specific identifier:"
|
|
72
|
+
lines << " • For App Groups: select your group.* identifier"
|
|
73
|
+
lines << " • For Apple Pay: select your merchant.* identifier"
|
|
74
|
+
lines << " 4. Regenerate the provisioning profile"
|
|
75
|
+
lines << ""
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Certificate mismatch
|
|
79
|
+
if cert_issues.any?
|
|
80
|
+
lines << " 🔐 CERTIFICATE MISMATCH"
|
|
81
|
+
lines << ""
|
|
82
|
+
lines << " Your app and its extensions are signed with different certificates."
|
|
83
|
+
lines << ""
|
|
84
|
+
lines << " How to fix:"
|
|
85
|
+
lines << " 1. Open your Xcode project"
|
|
86
|
+
lines << " 2. For EACH target (main app AND extensions):"
|
|
87
|
+
lines << " • Select the target → Signing & Capabilities"
|
|
88
|
+
lines << " • Ensure all targets use the same signing identity:"
|
|
89
|
+
lines << " - For App Store: 'Apple Distribution'"
|
|
90
|
+
lines << " - For Development: 'Apple Development'"
|
|
91
|
+
lines << " 3. Make sure all targets use matching profile types:"
|
|
92
|
+
lines << " • App Store profiles for App Store builds"
|
|
93
|
+
lines << " • Development profiles for development builds"
|
|
94
|
+
lines << ""
|
|
95
|
+
lines << " Quick fix for App Store builds:"
|
|
96
|
+
lines << " In project.pbxproj, ensure Release configuration has:"
|
|
97
|
+
lines << " CODE_SIGN_IDENTITY = \"Apple Distribution\""
|
|
98
|
+
lines << " PROVISIONING_PROFILE_SPECIFIER = \"YourApp App Store\""
|
|
99
|
+
lines << ""
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
lines << " 📚 More help:"
|
|
103
|
+
lines << " • Run 'mysigner doctor' to check your setup"
|
|
104
|
+
lines << " • Run 'mysigner profiles' to list available profiles"
|
|
105
|
+
lines << " • Check My Signer dashboard for Bundle ID capabilities"
|
|
106
|
+
lines << ""
|
|
107
|
+
|
|
108
|
+
lines.join("\n")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def analyze!
|
|
114
|
+
@build_errors.each do |error|
|
|
115
|
+
analyze_error(error)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def analyze_error(error)
|
|
120
|
+
# Normalize curly quotes to straight quotes
|
|
121
|
+
error = error.gsub(/["""]/, '"').gsub(/[''']/, "'")
|
|
122
|
+
|
|
123
|
+
# Pattern: Provisioning profile "X" doesn't include the Y capability
|
|
124
|
+
if match = error.match(/Provisioning profile "([^"]+)".*(?:doesn't|does not) include the (.+?) capability/i)
|
|
125
|
+
@issues << {
|
|
126
|
+
type: :profile_capability,
|
|
127
|
+
profile_name: match[1],
|
|
128
|
+
capability: match[2].strip
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Pattern: Provisioning profile "X" doesn't support the Y App Group
|
|
133
|
+
if match = error.match(/Provisioning profile "([^"]+)".*(?:doesn't|does not) support the (.+?) App Group/i)
|
|
134
|
+
@issues << {
|
|
135
|
+
type: :missing_identifier,
|
|
136
|
+
profile_name: match[1],
|
|
137
|
+
identifier_type: "App Group",
|
|
138
|
+
identifier: match[2].strip
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Pattern: Provisioning profile "X" doesn't support the Y Merchant ID
|
|
143
|
+
if match = error.match(/Provisioning profile "([^"]+)".*(?:doesn't|does not) support the (.+?) Merchant ID/i)
|
|
144
|
+
@issues << {
|
|
145
|
+
type: :missing_identifier,
|
|
146
|
+
profile_name: match[1],
|
|
147
|
+
identifier_type: "Merchant ID",
|
|
148
|
+
identifier: match[2].strip
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Pattern: Provisioning profile "X" doesn't match the entitlements file's value
|
|
153
|
+
if match = error.match(/Provisioning profile "([^"]+)".*(?:doesn't|does not) match.*entitlements.*?for the (.+?) entitlement/i)
|
|
154
|
+
capability = entitlement_to_capability(match[2])
|
|
155
|
+
@issues << {
|
|
156
|
+
type: :profile_capability,
|
|
157
|
+
profile_name: match[1],
|
|
158
|
+
capability: capability
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Pattern: Embedded binary is not signed with the same certificate
|
|
163
|
+
if error.include?("Embedded binary is not signed with the same certificate")
|
|
164
|
+
@issues << {
|
|
165
|
+
type: :certificate_mismatch,
|
|
166
|
+
message: error
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Pattern: Code Sign error
|
|
171
|
+
if error.include?("Code Sign error")
|
|
172
|
+
@issues << {
|
|
173
|
+
type: :code_sign_error,
|
|
174
|
+
message: error
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def entitlement_to_capability(entitlement)
|
|
180
|
+
mappings = {
|
|
181
|
+
"com.apple.security.application-groups" => "App Groups",
|
|
182
|
+
"com.apple.developer.in-app-payments" => "Apple Pay",
|
|
183
|
+
"aps-environment" => "Push Notifications",
|
|
184
|
+
"com.apple.developer.associated-domains" => "Associated Domains",
|
|
185
|
+
"com.apple.developer.applesignin" => "Sign in with Apple",
|
|
186
|
+
"com.apple.developer.icloud-services" => "iCloud",
|
|
187
|
+
"com.apple.developer.healthkit" => "HealthKit"
|
|
188
|
+
}
|
|
189
|
+
mappings[entitlement] || entitlement
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Mysigner
|
|
4
|
+
module Build
|
|
5
|
+
class Executor
|
|
6
|
+
class BuildError < StandardError; end
|
|
7
|
+
|
|
8
|
+
attr_reader :build_errors
|
|
9
|
+
|
|
10
|
+
def initialize(project_info, parser)
|
|
11
|
+
@project_info = project_info
|
|
12
|
+
@parser = parser
|
|
13
|
+
@build_errors = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Build archive
|
|
17
|
+
# Returns: path to .xcarchive
|
|
18
|
+
# Options:
|
|
19
|
+
# - signing_style: 'Automatic', 'Manual', or nil (default: use project setting)
|
|
20
|
+
# - team_id: Development team ID to override project setting
|
|
21
|
+
# - bundle_id: Bundle ID to override project setting
|
|
22
|
+
# - skip_extensions: If true, disable code signing for extension targets
|
|
23
|
+
def build!(target_name = nil, configuration = 'Release', scheme: nil, signing_style: nil, team_id: nil, bundle_id: nil, skip_extensions: false)
|
|
24
|
+
target = target_name || @parser.main_target.name
|
|
25
|
+
scheme_name = scheme || target
|
|
26
|
+
@signing_style = signing_style
|
|
27
|
+
@team_id = team_id
|
|
28
|
+
@bundle_id = bundle_id
|
|
29
|
+
@skip_extensions = skip_extensions
|
|
30
|
+
|
|
31
|
+
# Use Xcode's default DerivedData location to keep project clean
|
|
32
|
+
# This matches Xcode's behavior and avoids polluting the project directory
|
|
33
|
+
output_dir = File.join(@project_info[:directory], 'build')
|
|
34
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
|
35
|
+
|
|
36
|
+
# Generate archive path with timestamp
|
|
37
|
+
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
38
|
+
archive_name = "#{target}-#{timestamp}.xcarchive"
|
|
39
|
+
archive_path = File.join(output_dir, archive_name)
|
|
40
|
+
|
|
41
|
+
# Build command
|
|
42
|
+
cmd = build_command(scheme_name, configuration, archive_path)
|
|
43
|
+
|
|
44
|
+
# Execute build
|
|
45
|
+
success = execute_with_output(cmd)
|
|
46
|
+
|
|
47
|
+
unless success
|
|
48
|
+
raise BuildError, "Build failed. Check output above for errors."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Verify archive was created
|
|
52
|
+
unless File.exist?(archive_path)
|
|
53
|
+
raise BuildError, "Build reported success but archive not found at: #{archive_path}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
archive_path
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def build_command(scheme, configuration, archive_path)
|
|
62
|
+
cmd = ['xcodebuild', 'archive']
|
|
63
|
+
|
|
64
|
+
# Workspace or project
|
|
65
|
+
if @project_info[:type] == :workspace
|
|
66
|
+
cmd += ['-workspace', @project_info[:path]]
|
|
67
|
+
else
|
|
68
|
+
cmd += ['-project', @project_info[:path]]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Scheme and configuration
|
|
72
|
+
cmd += [
|
|
73
|
+
'-scheme', scheme,
|
|
74
|
+
'-configuration', configuration,
|
|
75
|
+
'-archivePath', archive_path
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# SDK selection based on platform
|
|
79
|
+
platform = @parser.target_platform(scheme)
|
|
80
|
+
sdk = case platform
|
|
81
|
+
when :macos
|
|
82
|
+
'macosx'
|
|
83
|
+
when :tvos
|
|
84
|
+
'appletvos'
|
|
85
|
+
when :watchos
|
|
86
|
+
'watchos'
|
|
87
|
+
else
|
|
88
|
+
'iphoneos' # default to iOS
|
|
89
|
+
end
|
|
90
|
+
cmd += ['-sdk', sdk]
|
|
91
|
+
|
|
92
|
+
# Override team ID if provided
|
|
93
|
+
if @team_id
|
|
94
|
+
cmd += ["DEVELOPMENT_TEAM=#{@team_id}"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Override bundle ID if provided
|
|
98
|
+
if @bundle_id
|
|
99
|
+
cmd += ["PRODUCT_BUNDLE_IDENTIFIER=#{@bundle_id}"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Handle signing based on style
|
|
103
|
+
case @signing_style
|
|
104
|
+
when 'Automatic'
|
|
105
|
+
# For automatic signing, allow Xcode to manage profiles
|
|
106
|
+
cmd += ['-allowProvisioningUpdates']
|
|
107
|
+
when 'Manual'
|
|
108
|
+
# For manual signing, don't override - project already configured
|
|
109
|
+
# No additional flags needed
|
|
110
|
+
else
|
|
111
|
+
# Default to automatic signing for simplicity
|
|
112
|
+
cmd += ['-allowProvisioningUpdates']
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Skip extension signing if requested
|
|
116
|
+
# This disables code signing for extension targets while keeping it enabled for the main app
|
|
117
|
+
if @skip_extensions && @parser.has_extensions?
|
|
118
|
+
@parser.extension_targets.each do |ext_target|
|
|
119
|
+
ext_name = ext_target.name
|
|
120
|
+
# Disable code signing for this extension target
|
|
121
|
+
cmd += ["CODE_SIGNING_ALLOWED[target=#{ext_name}]=NO"]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Suppress verbose output
|
|
126
|
+
cmd += [
|
|
127
|
+
'-quiet'
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
cmd.join(' ')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def execute_with_output(cmd)
|
|
134
|
+
puts "🏗️ Running: xcodebuild archive..."
|
|
135
|
+
puts ""
|
|
136
|
+
|
|
137
|
+
@build_errors = []
|
|
138
|
+
|
|
139
|
+
# Run command and capture output in real-time
|
|
140
|
+
IO.popen(cmd, err: [:child, :out]) do |io|
|
|
141
|
+
io.each_line do |line|
|
|
142
|
+
# Filter output to show only important messages
|
|
143
|
+
next if line.strip.empty?
|
|
144
|
+
|
|
145
|
+
# Detect error lines (case-insensitive for error:)
|
|
146
|
+
# Check for various error patterns including curly quotes from Xcode
|
|
147
|
+
is_error = line.downcase.include?('error:') ||
|
|
148
|
+
line.include?('Provisioning profile') ||
|
|
149
|
+
line.include?('Code Sign error') ||
|
|
150
|
+
line.include?("doesn't support") ||
|
|
151
|
+
line.include?("doesn\u2019t support") ||
|
|
152
|
+
line.include?('capability')
|
|
153
|
+
|
|
154
|
+
is_warning = line.downcase.include?('warning:')
|
|
155
|
+
|
|
156
|
+
# Show and capture errors and warnings
|
|
157
|
+
if is_error || is_warning
|
|
158
|
+
puts line
|
|
159
|
+
@build_errors << line if is_error
|
|
160
|
+
# Show progress markers
|
|
161
|
+
elsif line.include?('Building') || line.include?('Compiling') ||
|
|
162
|
+
line.include?('Linking') || line.include?('Signing') ||
|
|
163
|
+
line.include?('Copying')
|
|
164
|
+
print '.'
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
puts "" # New line after dots
|
|
170
|
+
|
|
171
|
+
$?.success?
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'xcodeproj'
|
|
2
|
+
|
|
3
|
+
module Mysigner
|
|
4
|
+
module Build
|
|
5
|
+
class Parser
|
|
6
|
+
attr_reader :project, :project_info
|
|
7
|
+
|
|
8
|
+
def initialize(project_info)
|
|
9
|
+
@project_info = project_info
|
|
10
|
+
@project = open_project
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get all target names
|
|
14
|
+
def targets
|
|
15
|
+
@project.targets.map(&:name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get all application targets (main apps only, no extensions)
|
|
19
|
+
def app_targets
|
|
20
|
+
@project.targets.select do |target|
|
|
21
|
+
target.product_type == 'com.apple.product-type.application'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get main app target (exclude test targets, extensions, etc.)
|
|
26
|
+
def main_target
|
|
27
|
+
# Return first app target, or first target if no app targets found
|
|
28
|
+
app_targets.first || @project.targets.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get all extension targets (widgets, share extensions, etc.)
|
|
32
|
+
def extension_targets
|
|
33
|
+
@project.targets.select do |target|
|
|
34
|
+
target.product_type&.include?('app-extension') ||
|
|
35
|
+
target.product_type&.include?('widget-extension') ||
|
|
36
|
+
target.product_type == 'com.apple.product-type.watchkit2-extension'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get all app + extension targets (everything that needs signing)
|
|
41
|
+
def all_app_targets
|
|
42
|
+
app_targets + extension_targets
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if project has extensions
|
|
46
|
+
def has_extensions?
|
|
47
|
+
extension_targets.any?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if project has multiple apps
|
|
51
|
+
def has_multiple_apps?
|
|
52
|
+
app_targets.count > 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get detailed info about a target
|
|
56
|
+
def target_info(target_name, configuration = 'Release')
|
|
57
|
+
target = find_target(target_name)
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
name: target.name,
|
|
61
|
+
type: product_type(target_name),
|
|
62
|
+
platform: target_platform(target_name),
|
|
63
|
+
bundle_id: bundle_id(target_name, configuration),
|
|
64
|
+
team_id: team_id(target_name, configuration),
|
|
65
|
+
signing_style: code_sign_style(target_name, configuration),
|
|
66
|
+
product_type: target.product_type
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get a list of all signable targets with their info
|
|
71
|
+
def signable_targets(configuration = 'Release')
|
|
72
|
+
all_app_targets.map do |target|
|
|
73
|
+
target_info(target.name, configuration)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Detect target platform (iOS, macOS, tvOS, watchOS)
|
|
78
|
+
def target_platform(target_name = nil)
|
|
79
|
+
target = find_target(target_name)
|
|
80
|
+
sdk = target.sdk
|
|
81
|
+
|
|
82
|
+
return :macos if sdk&.include?('macosx')
|
|
83
|
+
return :tvos if sdk&.include?('appletvos')
|
|
84
|
+
return :watchos if sdk&.include?('watchos')
|
|
85
|
+
:ios # default
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Detect product type (app, framework, library)
|
|
89
|
+
def product_type(target_name = nil)
|
|
90
|
+
target = find_target(target_name)
|
|
91
|
+
|
|
92
|
+
case target.product_type
|
|
93
|
+
when 'com.apple.product-type.application'
|
|
94
|
+
:app
|
|
95
|
+
when /framework/
|
|
96
|
+
:framework
|
|
97
|
+
when /library/
|
|
98
|
+
:library
|
|
99
|
+
when /app-extension/
|
|
100
|
+
:extension
|
|
101
|
+
else
|
|
102
|
+
:unknown
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get schemes (simplified - assume scheme name matches target name)
|
|
107
|
+
def schemes
|
|
108
|
+
# In reality, schemes are in xcshareddata/xcschemes/
|
|
109
|
+
# For now, return target names as potential schemes
|
|
110
|
+
targets
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get build settings for a target and configuration
|
|
114
|
+
def build_settings(target_name = nil, configuration = 'Release')
|
|
115
|
+
target = find_target(target_name)
|
|
116
|
+
config = target.build_configurations.find { |c| c.name == configuration }
|
|
117
|
+
|
|
118
|
+
raise "Configuration '#{configuration}' not found" unless config
|
|
119
|
+
|
|
120
|
+
config.build_settings
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get bundle identifier
|
|
124
|
+
def bundle_id(target_name = nil, configuration = 'Release')
|
|
125
|
+
settings = build_settings(target_name, configuration)
|
|
126
|
+
settings['PRODUCT_BUNDLE_IDENTIFIER']
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get development team
|
|
130
|
+
def team_id(target_name = nil, configuration = 'Release')
|
|
131
|
+
settings = build_settings(target_name, configuration)
|
|
132
|
+
settings['DEVELOPMENT_TEAM']
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get code sign identity
|
|
136
|
+
def code_sign_identity(target_name = nil, configuration = 'Release')
|
|
137
|
+
settings = build_settings(target_name, configuration)
|
|
138
|
+
settings['CODE_SIGN_IDENTITY']
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get provisioning profile specifier
|
|
142
|
+
def provisioning_profile(target_name = nil, configuration = 'Release')
|
|
143
|
+
settings = build_settings(target_name, configuration)
|
|
144
|
+
settings['PROVISIONING_PROFILE_SPECIFIER']
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get code sign style (Automatic or Manual)
|
|
148
|
+
def code_sign_style(target_name = nil, configuration = 'Release')
|
|
149
|
+
settings = build_settings(target_name, configuration)
|
|
150
|
+
settings['CODE_SIGN_STYLE']
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get all configurations
|
|
154
|
+
def configurations(target_name = nil)
|
|
155
|
+
target = find_target(target_name)
|
|
156
|
+
target.build_configurations.map(&:name)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if signing is configured
|
|
160
|
+
def signing_configured?(target_name = nil, configuration = 'Release')
|
|
161
|
+
profile = provisioning_profile(target_name, configuration)
|
|
162
|
+
identity = code_sign_identity(target_name, configuration)
|
|
163
|
+
|
|
164
|
+
!profile.to_s.empty? && !identity.to_s.empty?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get product name
|
|
168
|
+
def product_name(target_name = nil, configuration = 'Release')
|
|
169
|
+
settings = build_settings(target_name, configuration)
|
|
170
|
+
settings['PRODUCT_NAME'] || find_target(target_name).name
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Find a target by name (public method for use by other classes)
|
|
174
|
+
def find_target(target_name)
|
|
175
|
+
if target_name.nil?
|
|
176
|
+
return main_target
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
target = @project.targets.find { |t| t.name == target_name }
|
|
180
|
+
raise "Target '#{target_name}' not found" unless target
|
|
181
|
+
|
|
182
|
+
target
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def open_project
|
|
186
|
+
if @project_info[:type] == :workspace
|
|
187
|
+
# Workspace contains multiple projects
|
|
188
|
+
# Get the main project (not Pods)
|
|
189
|
+
workspace = Xcodeproj::Workspace.new_from_xcworkspace(@project_info[:path])
|
|
190
|
+
|
|
191
|
+
project_ref = workspace.file_references.find do |ref|
|
|
192
|
+
!ref.path.include?('Pods') && ref.path.end_with?('.xcodeproj')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
raise "No main project found in workspace" unless project_ref
|
|
196
|
+
|
|
197
|
+
project_path = File.join(File.dirname(@project_info[:path]), project_ref.path)
|
|
198
|
+
Xcodeproj::Project.open(project_path)
|
|
199
|
+
else
|
|
200
|
+
Xcodeproj::Project.open(@project_info[:path])
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|