DYXCFrameworkBuilder 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/.idea/.gitignore +8 -0
- data/.idea/DYXCFrameworkBuilder.iml +72 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/ARCHITECTURE.md +163 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +143 -0
- data/README.md +338 -0
- data/Rakefile +4 -0
- data/bin/xcbuilder +230 -0
- data/lib/DYXCFrameworkBuilder/builder.rb +136 -0
- data/lib/DYXCFrameworkBuilder/config.rb +122 -0
- data/lib/DYXCFrameworkBuilder/framework_podspec_generator.rb +157 -0
- data/lib/DYXCFrameworkBuilder/framework_publisher.rb +131 -0
- data/lib/DYXCFrameworkBuilder/oss_uploader.rb +210 -0
- data/lib/DYXCFrameworkBuilder/podspec_parser.rb +113 -0
- data/lib/DYXCFrameworkBuilder/version.rb +5 -0
- data/lib/DYXCFrameworkBuilder/xcframework_builder.rb +483 -0
- data/lib/DYXCFrameworkBuilder/yaml_generator.rb +292 -0
- data/lib/DYXCFrameworkBuilder.rb +45 -0
- data/sig/DYXCFrameworkBuilder.rbs +4 -0
- metadata +114 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DYXCFrameworkBuilder
|
4
|
+
class PodspecParser
|
5
|
+
attr_reader :name, :version, :deployment_target, :podspec_path, :podspec_dir
|
6
|
+
|
7
|
+
def initialize(podspec_path)
|
8
|
+
@podspec_path = File.expand_path(podspec_path)
|
9
|
+
@podspec_dir = File.dirname(@podspec_path)
|
10
|
+
|
11
|
+
unless File.exist?(@podspec_path)
|
12
|
+
raise DYXCFrameworkBuilder::Error, "Podspec file not found: #{@podspec_path}"
|
13
|
+
end
|
14
|
+
|
15
|
+
parse_podspec
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_workspace_path
|
19
|
+
example_dir = File.join(@podspec_dir, 'Example')
|
20
|
+
|
21
|
+
if Dir.exist?(example_dir)
|
22
|
+
# 查找 .xcworkspace 文件
|
23
|
+
workspace_files = Dir.glob(File.join(example_dir, '*.xcworkspace'))
|
24
|
+
return workspace_files.first if workspace_files.any?
|
25
|
+
|
26
|
+
# 如果没有找到 .xcworkspace,查找 .xcodeproj
|
27
|
+
project_files = Dir.glob(File.join(example_dir, '*.xcodeproj'))
|
28
|
+
return project_files.first if project_files.any?
|
29
|
+
end
|
30
|
+
|
31
|
+
raise DYXCFrameworkBuilder::Error, "No workspace or project file found in #{example_dir}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def output_directory
|
35
|
+
File.join(@podspec_dir, 'build')
|
36
|
+
end
|
37
|
+
|
38
|
+
def framework_name
|
39
|
+
"#{@name}.xcframework"
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_yaml_config
|
43
|
+
{
|
44
|
+
'project' => {
|
45
|
+
'path' => find_workspace_path,
|
46
|
+
'scheme' => @name,
|
47
|
+
'target' => @name,
|
48
|
+
'configuration' => 'Release'
|
49
|
+
},
|
50
|
+
'output' => {
|
51
|
+
'directory' => output_directory,
|
52
|
+
'framework_name' => framework_name
|
53
|
+
},
|
54
|
+
'platforms' => {
|
55
|
+
'ios' => {
|
56
|
+
'deployment_target' => @deployment_target || '11.0',
|
57
|
+
'architectures' => ['arm64']
|
58
|
+
},
|
59
|
+
'ios_simulator' => {
|
60
|
+
'deployment_target' => @deployment_target || '11.0',
|
61
|
+
'architectures' => ['arm64', 'x86_64']
|
62
|
+
}
|
63
|
+
},
|
64
|
+
'build_settings' => {
|
65
|
+
'enable_bitcode' => false,
|
66
|
+
'skip_warnings' => true,
|
67
|
+
'swift_version' => '5.0'
|
68
|
+
},
|
69
|
+
'metadata' => {
|
70
|
+
'generated_from_podspec' => @podspec_path,
|
71
|
+
'podspec_version' => @version,
|
72
|
+
'generated_at' => Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
73
|
+
}
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def parse_podspec
|
80
|
+
# 读取 podspec 文件内容
|
81
|
+
content = File.read(@podspec_path)
|
82
|
+
|
83
|
+
# 解析 name
|
84
|
+
if match = content.match(/s\.name\s*=\s*['"]([^'"]+)['"]/)
|
85
|
+
@name = match[1]
|
86
|
+
else
|
87
|
+
# 从文件名推断
|
88
|
+
@name = File.basename(@podspec_path, '.podspec')
|
89
|
+
end
|
90
|
+
|
91
|
+
# 解析 version
|
92
|
+
if match = content.match(/s\.version\s*=\s*['"]([^'"]+)['"]/)
|
93
|
+
@version = match[1]
|
94
|
+
end
|
95
|
+
|
96
|
+
# 解析 deployment_target
|
97
|
+
if match = content.match(/s\.ios\.deployment_target\s*=\s*['"]([^'"]+)['"]/)
|
98
|
+
@deployment_target = match[1]
|
99
|
+
elsif match = content.match(/s\.deployment_target\s*=\s*['"]([^'"]+)['"]/)
|
100
|
+
@deployment_target = match[1]
|
101
|
+
end
|
102
|
+
|
103
|
+
validate_parsed_data
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_parsed_data
|
107
|
+
raise DYXCFrameworkBuilder::Error, "Could not parse name from podspec: #{@podspec_path}" if @name.nil? || @name.empty?
|
108
|
+
|
109
|
+
puts "[INFO] Parsed podspec: #{@name} (#{@version || 'unknown version'})"
|
110
|
+
puts "[INFO] Deployment target: #{@deployment_target || 'using default 11.0'}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,483 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module DYXCFrameworkBuilder
|
6
|
+
# Core XCFramework building logic
|
7
|
+
class CoreBuilder
|
8
|
+
attr_reader :config, :verbose
|
9
|
+
|
10
|
+
def initialize(config, verbose: false)
|
11
|
+
@config = config
|
12
|
+
@verbose = verbose
|
13
|
+
end
|
14
|
+
|
15
|
+
def build
|
16
|
+
log "Building XCFramework from configuration"
|
17
|
+
log "Project: #{@config.project_path}"
|
18
|
+
log "Scheme: #{@config.scheme}"
|
19
|
+
log "Configuration: #{@config.configuration}"
|
20
|
+
log "Output Directory: #{@config.output_dir}"
|
21
|
+
|
22
|
+
# Create output directory
|
23
|
+
FileUtils.mkdir_p(@config.output_dir)
|
24
|
+
|
25
|
+
# Build for each platform
|
26
|
+
frameworks = []
|
27
|
+
|
28
|
+
if @config.platforms['ios']
|
29
|
+
log "Building for iOS device..."
|
30
|
+
ios_framework = build_for_platform('iphoneos', @config.platforms['ios'])
|
31
|
+
frameworks << ios_framework if ios_framework
|
32
|
+
end
|
33
|
+
|
34
|
+
if @config.platforms['ios_simulator']
|
35
|
+
log "Building for iOS Simulator..."
|
36
|
+
sim_framework = build_for_platform('iphonesimulator', @config.platforms['ios_simulator'])
|
37
|
+
frameworks << sim_framework if sim_framework
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create XCFramework
|
41
|
+
if frameworks.any?
|
42
|
+
create_xcframework(frameworks)
|
43
|
+
else
|
44
|
+
raise Error, "No frameworks were built successfully"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def build_for_platform(sdk, platform_config)
|
51
|
+
deployment_target = platform_config['deployment_target'] || '11.0'
|
52
|
+
architectures = filter_architectures_for_sdk(platform_config['architectures'] || ['arm64'], sdk)
|
53
|
+
|
54
|
+
log "Building for #{sdk} with architectures: #{architectures.join(', ')}"
|
55
|
+
|
56
|
+
# Create derived data path
|
57
|
+
derived_data_path = File.join(@config.output_dir, "DerivedData")
|
58
|
+
|
59
|
+
# For modern Xcode, we can build all architectures in one go instead of separate archives
|
60
|
+
if architectures.length == 1 || should_build_universal_archive(sdk)
|
61
|
+
# Build single archive with multiple architectures
|
62
|
+
archive_path = File.join(@config.output_dir, "#{@config.scheme}-#{sdk}.xcarchive")
|
63
|
+
|
64
|
+
build_cmd = build_archive_command(sdk, architectures, deployment_target, archive_path, derived_data_path)
|
65
|
+
|
66
|
+
log "Executing: #{build_cmd}" if @verbose
|
67
|
+
|
68
|
+
success = execute_command(build_cmd)
|
69
|
+
|
70
|
+
if success
|
71
|
+
log "✅ Successfully built archive for #{sdk} with architectures: #{architectures.join(', ')}"
|
72
|
+
framework_path = create_framework_from_archives([archive_path], sdk)
|
73
|
+
return framework_path
|
74
|
+
else
|
75
|
+
log "❌ Failed to build archive for #{sdk}"
|
76
|
+
end
|
77
|
+
else
|
78
|
+
# Fallback: Build separate archives for each architecture
|
79
|
+
archive_paths = []
|
80
|
+
|
81
|
+
architectures.each do |arch|
|
82
|
+
archive_path = File.join(@config.output_dir, "#{@config.scheme}-#{sdk}-#{arch}.xcarchive")
|
83
|
+
|
84
|
+
build_cmd = build_archive_command(sdk, [arch], deployment_target, archive_path, derived_data_path)
|
85
|
+
|
86
|
+
log "Executing: #{build_cmd}" if @verbose
|
87
|
+
|
88
|
+
success = execute_command(build_cmd)
|
89
|
+
|
90
|
+
if success
|
91
|
+
archive_paths << archive_path
|
92
|
+
log "✅ Successfully built archive for #{sdk} #{arch}"
|
93
|
+
else
|
94
|
+
log "❌ Failed to build archive for #{sdk} #{arch}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
if archive_paths.any?
|
99
|
+
# Create framework from archives
|
100
|
+
framework_path = create_framework_from_archives(archive_paths, sdk)
|
101
|
+
return framework_path
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_archive_command(sdk, architectures, deployment_target, archive_path, derived_data_path)
|
109
|
+
# Determine whether to use -workspace or -project based on file extension
|
110
|
+
project_flag, project_path = determine_project_type(@config.project_path)
|
111
|
+
|
112
|
+
# Ensure architectures is an array
|
113
|
+
arch_list = Array(architectures)
|
114
|
+
|
115
|
+
cmd = [
|
116
|
+
"xcodebuild",
|
117
|
+
"archive",
|
118
|
+
project_flag, "\"#{project_path}\"",
|
119
|
+
"-scheme", "\"#{@config.scheme}\"",
|
120
|
+
"-configuration", @config.configuration,
|
121
|
+
"-destination", "\"generic/platform=#{platform_destination(sdk)}\"",
|
122
|
+
"-archivePath", "\"#{archive_path}\"",
|
123
|
+
"-derivedDataPath", "\"#{derived_data_path}\"",
|
124
|
+
"SKIP_INSTALL=NO",
|
125
|
+
"BUILD_LIBRARY_FOR_DISTRIBUTION=YES"
|
126
|
+
]
|
127
|
+
|
128
|
+
# Add target if specified and different from scheme
|
129
|
+
if @config.target && @config.target != @config.scheme
|
130
|
+
cmd += ["-target", "\"#{@config.target}\""]
|
131
|
+
end
|
132
|
+
|
133
|
+
# Add architectures if multiple specified
|
134
|
+
if arch_list.length > 1
|
135
|
+
cmd << "ARCHS=#{arch_list.join(' ')}"
|
136
|
+
cmd << "VALID_ARCHS=#{arch_list.join(' ')}"
|
137
|
+
elsif arch_list.length == 1
|
138
|
+
cmd << "ARCHS=#{arch_list.first}"
|
139
|
+
cmd << "VALID_ARCHS=#{arch_list.first}"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Add deployment target
|
143
|
+
case sdk
|
144
|
+
when 'iphoneos', 'iphonesimulator'
|
145
|
+
cmd << "IPHONEOS_DEPLOYMENT_TARGET=#{deployment_target}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Skip warnings if configured
|
149
|
+
if @config.skip_warnings
|
150
|
+
cmd << "GCC_WARN_INHIBIT_ALL_WARNINGS=YES"
|
151
|
+
cmd << "SWIFT_SUPPRESS_WARNINGS=YES"
|
152
|
+
cmd << "CLANG_WARN_DOCUMENTATION_COMMENTS=NO"
|
153
|
+
cmd << "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER=NO"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Disable bitcode if configured
|
157
|
+
unless @config.enable_bitcode
|
158
|
+
cmd << "ENABLE_BITCODE=NO"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Additional build settings for better compatibility
|
162
|
+
cmd << "ONLY_ACTIVE_ARCH=NO"
|
163
|
+
cmd << "DEFINES_MODULE=YES"
|
164
|
+
|
165
|
+
cmd.join(" ")
|
166
|
+
end
|
167
|
+
|
168
|
+
def platform_destination(sdk)
|
169
|
+
case sdk
|
170
|
+
when 'iphoneos'
|
171
|
+
'iOS'
|
172
|
+
when 'iphonesimulator'
|
173
|
+
'iOS Simulator'
|
174
|
+
else
|
175
|
+
sdk
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def determine_project_type(project_path)
|
180
|
+
case File.extname(project_path).downcase
|
181
|
+
when '.xcworkspace'
|
182
|
+
['-workspace', project_path]
|
183
|
+
when '.xcodeproj'
|
184
|
+
['-project', project_path]
|
185
|
+
else
|
186
|
+
# Default to workspace if no extension or unknown extension
|
187
|
+
# Most modern iOS projects use workspaces
|
188
|
+
if File.exist?("#{project_path}.xcworkspace")
|
189
|
+
['-workspace', "#{project_path}.xcworkspace"]
|
190
|
+
elsif File.exist?("#{project_path}.xcodeproj")
|
191
|
+
['-project', "#{project_path}.xcodeproj"]
|
192
|
+
else
|
193
|
+
# Assume workspace as default for modern projects
|
194
|
+
['-workspace', project_path]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def filter_architectures_for_sdk(architectures, sdk)
|
200
|
+
case sdk
|
201
|
+
when 'iphoneos'
|
202
|
+
# iOS device: only arm64
|
203
|
+
architectures.select { |arch| ['arm64'].include?(arch) }
|
204
|
+
when 'iphonesimulator'
|
205
|
+
# iOS simulator: arm64 and x86_64
|
206
|
+
architectures.select { |arch| ['arm64', 'x86_64'].include?(arch) }
|
207
|
+
else
|
208
|
+
architectures
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def should_build_universal_archive(sdk)
|
213
|
+
# Modern Xcode can build universal archives in one go for most cases
|
214
|
+
true
|
215
|
+
end
|
216
|
+
|
217
|
+
def fix_framework_binary_name(framework_path, sdk)
|
218
|
+
# Keep the original scheme name for the binary (no SDK suffix)
|
219
|
+
target_binary_path = File.join(framework_path, @config.scheme)
|
220
|
+
|
221
|
+
# Check if binary already exists with correct name
|
222
|
+
if File.exist?(target_binary_path)
|
223
|
+
log "✅ Framework binary verified: #{target_binary_path}" if @verbose
|
224
|
+
else
|
225
|
+
# Try to find any binary file in the framework and rename it
|
226
|
+
found_binary = false
|
227
|
+
Dir.glob(File.join(framework_path, "*")).each do |file|
|
228
|
+
next if File.directory?(file)
|
229
|
+
next if File.extname(file) != "" # Skip files with extensions
|
230
|
+
next if file.include?('Info.plist')
|
231
|
+
|
232
|
+
# Check if it's likely a binary file
|
233
|
+
if File.executable?(file) || File.size(file) > 1024 # Assume binary if executable or large enough
|
234
|
+
log "Renaming binary from #{File.basename(file)} to #{@config.scheme}" if @verbose
|
235
|
+
FileUtils.mv(file, target_binary_path)
|
236
|
+
found_binary = true
|
237
|
+
break
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
unless found_binary
|
242
|
+
log "❌ Warning: No binary found in framework: #{framework_path}"
|
243
|
+
return nil
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Verify the binary file exists and has correct permissions
|
248
|
+
if File.exist?(target_binary_path)
|
249
|
+
FileUtils.chmod(0755, target_binary_path) unless File.executable?(target_binary_path)
|
250
|
+
log "✅ Framework binary ready: #{target_binary_path}" if @verbose
|
251
|
+
return target_binary_path
|
252
|
+
end
|
253
|
+
|
254
|
+
nil
|
255
|
+
end
|
256
|
+
|
257
|
+
def update_framework_references(framework_path, old_name, new_name)
|
258
|
+
# Update Info.plist if it exists
|
259
|
+
info_plist_path = File.join(framework_path, 'Info.plist')
|
260
|
+
if File.exist?(info_plist_path)
|
261
|
+
content = File.read(info_plist_path)
|
262
|
+
updated_content = content.gsub(old_name, new_name)
|
263
|
+
File.write(info_plist_path, updated_content) if content != updated_content
|
264
|
+
end
|
265
|
+
|
266
|
+
# Update module.modulemap if it exists
|
267
|
+
modulemap_paths = [
|
268
|
+
File.join(framework_path, 'Modules', 'module.modulemap'),
|
269
|
+
File.join(framework_path, 'Headers', 'module.modulemap')
|
270
|
+
]
|
271
|
+
|
272
|
+
modulemap_paths.each do |modulemap_path|
|
273
|
+
if File.exist?(modulemap_path)
|
274
|
+
content = File.read(modulemap_path)
|
275
|
+
updated_content = content.gsub(/framework module #{old_name}/, "framework module #{new_name}")
|
276
|
+
File.write(modulemap_path, updated_content) if content != updated_content
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def create_framework_from_archives(archive_paths, sdk)
|
282
|
+
log "Creating framework from #{archive_paths.length} archive(s) for #{sdk}"
|
283
|
+
|
284
|
+
# Use temporary SDK-specific name during build, will be renamed for XCFramework
|
285
|
+
temp_framework_path = File.join(@config.output_dir, "#{@config.scheme}-#{sdk}.framework")
|
286
|
+
|
287
|
+
# Remove existing framework if it exists
|
288
|
+
FileUtils.rm_rf(temp_framework_path) if File.exist?(temp_framework_path)
|
289
|
+
|
290
|
+
if archive_paths.length == 1
|
291
|
+
# Single architecture - just copy the framework
|
292
|
+
archive_framework_path = find_framework_in_archive(archive_paths.first)
|
293
|
+
if archive_framework_path && File.exist?(archive_framework_path)
|
294
|
+
FileUtils.cp_r(archive_framework_path, temp_framework_path)
|
295
|
+
|
296
|
+
# Rename to final framework name
|
297
|
+
final_framework_path = File.join(@config.output_dir, "#{@config.scheme}.framework")
|
298
|
+
FileUtils.rm_rf(final_framework_path) if File.exist?(final_framework_path)
|
299
|
+
FileUtils.mv(temp_framework_path, final_framework_path)
|
300
|
+
|
301
|
+
# Fix binary name to use scheme name
|
302
|
+
fix_framework_binary_name(final_framework_path, sdk)
|
303
|
+
|
304
|
+
log "✅ Created framework: #{final_framework_path}"
|
305
|
+
return final_framework_path
|
306
|
+
end
|
307
|
+
else
|
308
|
+
# Multiple architectures - create universal binary
|
309
|
+
log "Creating universal framework for #{sdk}..."
|
310
|
+
universal_framework_path = create_universal_framework(archive_paths, sdk)
|
311
|
+
return universal_framework_path if universal_framework_path
|
312
|
+
end
|
313
|
+
|
314
|
+
log "⚠️ Warning: Could not find framework in archives for #{sdk}"
|
315
|
+
nil
|
316
|
+
end
|
317
|
+
|
318
|
+
def find_framework_in_archive(archive_path)
|
319
|
+
# Try different possible paths for the framework in the archive
|
320
|
+
possible_paths = [
|
321
|
+
File.join(archive_path, "Products/Library/Frameworks/#{@config.scheme}.framework"),
|
322
|
+
File.join(archive_path, "Products/@rpath/#{@config.scheme}.framework"),
|
323
|
+
File.join(archive_path, "Products/usr/local/lib/#{@config.scheme}.framework")
|
324
|
+
]
|
325
|
+
|
326
|
+
possible_paths.each do |path|
|
327
|
+
return path if File.exist?(path)
|
328
|
+
end
|
329
|
+
|
330
|
+
# If not found, search recursively
|
331
|
+
framework_name = "#{@config.scheme}.framework"
|
332
|
+
Dir.glob(File.join(archive_path, "**/#{framework_name}")).first
|
333
|
+
end
|
334
|
+
|
335
|
+
def create_universal_framework(archive_paths, sdk)
|
336
|
+
log "Creating universal binary for multiple architectures"
|
337
|
+
|
338
|
+
# Use temporary SDK-specific name during build
|
339
|
+
temp_framework_path = File.join(@config.output_dir, "#{@config.scheme}-#{sdk}.framework")
|
340
|
+
temp_frameworks = []
|
341
|
+
binary_architectures = []
|
342
|
+
|
343
|
+
# Extract all frameworks and check architectures
|
344
|
+
archive_paths.each_with_index do |archive_path, index|
|
345
|
+
archive_framework_path = find_framework_in_archive(archive_path)
|
346
|
+
if archive_framework_path && File.exist?(archive_framework_path)
|
347
|
+
temp_framework_path = File.join(@config.output_dir, "temp_#{sdk}_#{index}.framework")
|
348
|
+
FileUtils.cp_r(archive_framework_path, temp_framework_path)
|
349
|
+
|
350
|
+
# Check binary architecture
|
351
|
+
binary_path = File.join(temp_framework_path, @config.scheme)
|
352
|
+
if File.exist?(binary_path)
|
353
|
+
arch_info = `lipo -info "#{binary_path}" 2>/dev/null`.strip
|
354
|
+
log "Framework #{index} architecture: #{arch_info}" if @verbose
|
355
|
+
|
356
|
+
# Extract architecture from lipo output
|
357
|
+
if arch_info.include?(":")
|
358
|
+
arch = arch_info.split(":").last.strip
|
359
|
+
unless binary_architectures.include?(arch)
|
360
|
+
binary_architectures << arch
|
361
|
+
temp_frameworks << temp_framework_path
|
362
|
+
else
|
363
|
+
log "⚠️ Skipping duplicate architecture: #{arch}"
|
364
|
+
FileUtils.rm_rf(temp_framework_path)
|
365
|
+
end
|
366
|
+
else
|
367
|
+
temp_frameworks << temp_framework_path
|
368
|
+
end
|
369
|
+
else
|
370
|
+
temp_frameworks << temp_framework_path
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
if temp_frameworks.empty?
|
376
|
+
log "❌ No frameworks found in archives"
|
377
|
+
return nil
|
378
|
+
end
|
379
|
+
|
380
|
+
# Use the first framework as base
|
381
|
+
base_framework = temp_frameworks.first
|
382
|
+
FileUtils.cp_r(base_framework, temp_framework_path)
|
383
|
+
|
384
|
+
# Create universal binary if we have multiple frameworks with different architectures
|
385
|
+
if temp_frameworks.length > 1
|
386
|
+
# Use the original scheme name for the binary
|
387
|
+
binary_path = File.join(temp_framework_path, @config.scheme)
|
388
|
+
all_binaries = temp_frameworks.map { |fw| File.join(fw, @config.scheme) }.select { |b| File.exist?(b) }
|
389
|
+
|
390
|
+
if all_binaries.length > 1
|
391
|
+
# Use lipo to create universal binary
|
392
|
+
lipo_cmd = "lipo -create #{all_binaries.map { |b| "\"#{b}\"" }.join(' ')} -output \"#{binary_path}\""
|
393
|
+
|
394
|
+
log "Creating universal binary: #{lipo_cmd}" if @verbose
|
395
|
+
|
396
|
+
if execute_command(lipo_cmd)
|
397
|
+
log "✅ Successfully created universal framework with architectures: #{binary_architectures.join(', ')}"
|
398
|
+
else
|
399
|
+
log "⚠️ Warning: Failed to create universal binary, using single architecture"
|
400
|
+
end
|
401
|
+
else
|
402
|
+
log "✅ Using single architecture framework"
|
403
|
+
end
|
404
|
+
else
|
405
|
+
log "✅ Using single framework (only one valid architecture)"
|
406
|
+
end
|
407
|
+
|
408
|
+
# Clean up temporary frameworks
|
409
|
+
temp_frameworks.each { |fw| FileUtils.rm_rf(fw) }
|
410
|
+
|
411
|
+
# Rename to final framework name
|
412
|
+
final_framework_path = File.join(@config.output_dir, "#{@config.scheme}.framework")
|
413
|
+
FileUtils.rm_rf(final_framework_path) if File.exist?(final_framework_path)
|
414
|
+
FileUtils.mv(temp_framework_path, final_framework_path)
|
415
|
+
|
416
|
+
# Fix binary name to use scheme name
|
417
|
+
fix_framework_binary_name(final_framework_path, sdk)
|
418
|
+
|
419
|
+
final_framework_path
|
420
|
+
end
|
421
|
+
|
422
|
+
def create_xcframework(frameworks)
|
423
|
+
xcframework_path = File.join(@config.output_dir, @config.framework_name)
|
424
|
+
|
425
|
+
log "Creating XCFramework at: #{xcframework_path}"
|
426
|
+
|
427
|
+
# Remove existing xcframework if it exists
|
428
|
+
FileUtils.rm_rf(xcframework_path) if File.exist?(xcframework_path)
|
429
|
+
|
430
|
+
cmd = [
|
431
|
+
"xcodebuild",
|
432
|
+
"-create-xcframework"
|
433
|
+
]
|
434
|
+
|
435
|
+
frameworks.each do |framework|
|
436
|
+
cmd << "-framework" << "\"#{framework}\""
|
437
|
+
end
|
438
|
+
|
439
|
+
cmd << "-output" << "\"#{xcframework_path}\""
|
440
|
+
|
441
|
+
build_cmd = cmd.join(" ")
|
442
|
+
log "Executing: #{build_cmd}" if @verbose
|
443
|
+
|
444
|
+
success = execute_command(build_cmd)
|
445
|
+
|
446
|
+
if success
|
447
|
+
# Verify the XCFramework was created
|
448
|
+
if File.exist?(xcframework_path)
|
449
|
+
log "🎉 Successfully created XCFramework: #{xcframework_path}"
|
450
|
+
|
451
|
+
# Show XCFramework info
|
452
|
+
info_cmd = "xcodebuild -checkFirstLaunchForXcode"
|
453
|
+
execute_command("#{info_cmd} > /dev/null 2>&1") # Suppress output
|
454
|
+
|
455
|
+
# Clean up intermediate frameworks
|
456
|
+
frameworks.each { |fw| FileUtils.rm_rf(fw) }
|
457
|
+
|
458
|
+
return xcframework_path
|
459
|
+
else
|
460
|
+
raise Error, "XCFramework was not created at expected path: #{xcframework_path}"
|
461
|
+
end
|
462
|
+
else
|
463
|
+
raise Error, "Failed to create XCFramework"
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
def execute_command(command)
|
468
|
+
if @verbose
|
469
|
+
system(command)
|
470
|
+
else
|
471
|
+
system("#{command} > /dev/null 2>&1")
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def log(message)
|
476
|
+
if @verbose
|
477
|
+
puts "[INFO] #{message}"
|
478
|
+
else
|
479
|
+
puts message
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|