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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +7 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +137 -0
  10. data/LICENSE +201 -0
  11. data/MANUAL_TEST.md +341 -0
  12. data/README.md +493 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/exe/mysigner +5 -0
  17. data/lib/mysigner/build/android_executor.rb +367 -0
  18. data/lib/mysigner/build/android_parser.rb +293 -0
  19. data/lib/mysigner/build/configurator.rb +126 -0
  20. data/lib/mysigner/build/detector.rb +388 -0
  21. data/lib/mysigner/build/error_analyzer.rb +193 -0
  22. data/lib/mysigner/build/executor.rb +176 -0
  23. data/lib/mysigner/build/parser.rb +206 -0
  24. data/lib/mysigner/cli/auth_commands.rb +1381 -0
  25. data/lib/mysigner/cli/build_commands.rb +2095 -0
  26. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +500 -0
  27. data/lib/mysigner/cli/concerns/api_helpers.rb +131 -0
  28. data/lib/mysigner/cli/concerns/error_handlers.rb +446 -0
  29. data/lib/mysigner/cli/concerns/helpers.rb +63 -0
  30. data/lib/mysigner/cli/diagnostic_commands.rb +1034 -0
  31. data/lib/mysigner/cli/resource_commands.rb +2670 -0
  32. data/lib/mysigner/cli.rb +43 -0
  33. data/lib/mysigner/client.rb +189 -0
  34. data/lib/mysigner/config.rb +311 -0
  35. data/lib/mysigner/export/exporter.rb +150 -0
  36. data/lib/mysigner/signing/certificate_checker.rb +148 -0
  37. data/lib/mysigner/signing/keystore_manager.rb +239 -0
  38. data/lib/mysigner/signing/validator.rb +150 -0
  39. data/lib/mysigner/signing/wizard.rb +784 -0
  40. data/lib/mysigner/upload/app_store_automation.rb +402 -0
  41. data/lib/mysigner/upload/app_store_submission.rb +312 -0
  42. data/lib/mysigner/upload/play_store_uploader.rb +378 -0
  43. data/lib/mysigner/upload/uploader.rb +373 -0
  44. data/lib/mysigner/version.rb +3 -0
  45. data/lib/mysigner.rb +15 -0
  46. data/mysigner.gemspec +78 -0
  47. data/test_manual.rb +102 -0
  48. 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
+