xcframework_converter 0.1.3 → 0.2.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 +4 -4
- data/lib/xcframework_converter/arm_patcher.rb +78 -0
- data/lib/xcframework_converter/creation.rb +42 -0
- data/lib/xcframework_converter/patching.rb +58 -0
- data/lib/xcframework_converter/version.rb +1 -1
- data/lib/xcframework_converter/xcframework_ext.rb +19 -0
- data/lib/xcframework_converter.rb +30 -94
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 105e63e07d147cd791c478f9351e73e1f71bbd861160b5a1391d99c7c0affc9d
|
4
|
+
data.tar.gz: cf2e6a358aeb0a7018d06a2bf48382ab01d768eb870589da0abddab7044278e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe005277c05790de2d906188fbc0ab3818b9dd27980bf68bdeb2e56ec3ef2f0184fc61224ddeb32c29689ad316f3179eb51dd4a3345bf423bdd7cd5d75569697
|
7
|
+
data.tar.gz: '0805b456eebbfc655bd5991c001b241b29bf614f154615be67be7124d3bc1fc95532a0612647ac1d56e05d10373c97c7f6e14489adb68481e24e6b5a03ff89c6'
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cocoapods'
|
4
|
+
require 'cocoapods/xcode/xcframework'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'xcodeproj'
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
9
|
+
|
10
|
+
module XCFrameworkConverter
|
11
|
+
# Patches a binary (static or dynamic), turning an arm64-device into an arm64-simualtor.
|
12
|
+
# For more info:
|
13
|
+
# static libs: https://bogo.wtf/arm64-to-sim.html
|
14
|
+
# dylibs: https://bogo.wtf/arm64-to-sim-dylibs.html
|
15
|
+
module ArmPatcher
|
16
|
+
class << self
|
17
|
+
def patch_arm_binary(slice)
|
18
|
+
require 'macho'
|
19
|
+
|
20
|
+
case slice.build_type.linkage
|
21
|
+
when :dynamic
|
22
|
+
patch_arm_binary_dynamic(slice)
|
23
|
+
when :static
|
24
|
+
patch_arm_binary_static(slice)
|
25
|
+
end
|
26
|
+
|
27
|
+
slice.path.glob('**/arm64*.swiftinterface').each do |interface_file|
|
28
|
+
`sed -i '' -E 's/target arm64-apple-ios([0-9.]+) /target arm64-apple-ios\\1-simulator /g' "#{interface_file}"`
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def patch_arm_binary_dynamic(slice)
|
35
|
+
extracted_path = slice.path.join('arm64.dylib')
|
36
|
+
`xcrun lipo \"#{slice.binary_path}\" -thin arm64 -output \"#{extracted_path}\"`
|
37
|
+
|
38
|
+
file = MachO::MachOFile.new(extracted_path)
|
39
|
+
sdk_version = file[:LC_VERSION_MIN_IPHONEOS].first.version_string
|
40
|
+
`xcrun vtool -arch arm64 -set-build-version 7 #{sdk_version} #{sdk_version} -replace -output \"#{extracted_path}\" \"#{extracted_path}\"`
|
41
|
+
`xcrun lipo \"#{slice.binary_path}\" -replace arm64 \"#{extracted_path}\" -output \"#{slice.binary_path}\"`
|
42
|
+
extracted_path.rmtree
|
43
|
+
end
|
44
|
+
|
45
|
+
def arm2sim_path
|
46
|
+
Pathname.new(__FILE__).dirname.join('../arm2sim.swift')
|
47
|
+
end
|
48
|
+
|
49
|
+
def patch_arm_binary_static(slice)
|
50
|
+
extracted_path = slice.path.join('arm64.a')
|
51
|
+
`xcrun lipo \"#{slice.binary_path}\" -thin arm64 -output \"#{extracted_path}\"`
|
52
|
+
extracted_path_dir = slice.path.join('arm64-objects')
|
53
|
+
extracted_path_dir.mkdir
|
54
|
+
`cd \"#{extracted_path_dir}\" ; ar x \"#{extracted_path}\"`
|
55
|
+
Dir[extracted_path_dir.join('*.o')].each do |object_file|
|
56
|
+
file = MachO::MachOFile.new(object_file)
|
57
|
+
sdk_version = file[:LC_VERSION_MIN_IPHONEOS].first.version_string.to_i
|
58
|
+
`xcrun swift \"#{arm2sim_path}\" \"#{object_file}\" \"#{sdk_version}\" \"#{sdk_version}\"`
|
59
|
+
end
|
60
|
+
`cd \"#{extracted_path_dir}\" ; ar crv \"#{extracted_path}\" *.o`
|
61
|
+
|
62
|
+
`xcrun lipo \"#{slice.binary_path}\" -replace arm64 \"#{extracted_path}\" -output \"#{slice.binary_path}\"`
|
63
|
+
extracted_path_dir.rmtree
|
64
|
+
extracted_path.rmtree
|
65
|
+
end
|
66
|
+
|
67
|
+
public
|
68
|
+
|
69
|
+
def cleanup_unused_archs(slice)
|
70
|
+
supported_archs = slice.supported_archs
|
71
|
+
unsupported_archs = `xcrun lipo \"#{slice.binary_path}\" -archs`.split - supported_archs
|
72
|
+
unsupported_archs.each do |arch|
|
73
|
+
`xcrun lipo \"#{slice.binary_path}\" -remove \"#{arch}\" -output \"#{slice.binary_path}\"`
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'arm_patcher'
|
4
|
+
require_relative 'xcframework_ext'
|
5
|
+
|
6
|
+
require 'cocoapods'
|
7
|
+
require 'cocoapods/xcode/xcframework'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'xcodeproj'
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/AbcSize
|
12
|
+
|
13
|
+
# Converts a framework (static or dynamic) to an XCFramework, adding an arm64 simulator patch.
|
14
|
+
# For more info:
|
15
|
+
# static libs: https://bogo.wtf/arm64-to-sim.html
|
16
|
+
# dylibs: https://bogo.wtf/arm64-to-sim-dylibs.html
|
17
|
+
module XCFrameworkConverter
|
18
|
+
class << self
|
19
|
+
def plist_template_path
|
20
|
+
Pathname.new(__FILE__).dirname.join('../xcframework_template.plist')
|
21
|
+
end
|
22
|
+
|
23
|
+
def convert_framework_to_xcframework(path)
|
24
|
+
plist = Xcodeproj::Plist.read_from_path(plist_template_path)
|
25
|
+
xcframework_path = Pathname.new(path).sub_ext('.xcframework')
|
26
|
+
xcframework_path.mkdir
|
27
|
+
plist['AvailableLibraries'].each do |slice|
|
28
|
+
slice_path = xcframework_path.join(slice['LibraryIdentifier'])
|
29
|
+
slice_path.mkdir
|
30
|
+
slice['LibraryPath'] = File.basename(path)
|
31
|
+
FileUtils.cp_r(path, slice_path)
|
32
|
+
end
|
33
|
+
Xcodeproj::Plist.write_to_path(plist, xcframework_path.join('Info.plist'))
|
34
|
+
FileUtils.rm_rf(path)
|
35
|
+
final_framework = Pod::Xcode::XCFramework.open_xcframework(xcframework_path)
|
36
|
+
final_framework.slices.each do |slice|
|
37
|
+
ArmPatcher.patch_arm_binary(slice) if slice.platform == :ios && slice.platform_variant == :simulator
|
38
|
+
ArmPatcher.cleanup_unused_archs(slice)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'arm_patcher'
|
4
|
+
require_relative 'xcframework_ext'
|
5
|
+
|
6
|
+
require 'cocoapods'
|
7
|
+
require 'cocoapods/xcode/xcframework'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'xcodeproj'
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
12
|
+
|
13
|
+
# Converts a framework (static or dynamic) to an XCFramework, adding an arm64 simulator patch.
|
14
|
+
# For more info:
|
15
|
+
# static libs: https://bogo.wtf/arm64-to-sim.html
|
16
|
+
# dylibs: https://bogo.wtf/arm64-to-sim-dylibs.html
|
17
|
+
module XCFrameworkConverter
|
18
|
+
class << self
|
19
|
+
def patch_xcframework(xcframework_path)
|
20
|
+
xcframework = Pod::Xcode::XCFramework.open_xcframework(xcframework_path)
|
21
|
+
|
22
|
+
return nil if xcframework.slices.any? do |slice|
|
23
|
+
slice.platform == :ios &&
|
24
|
+
slice.platform_variant == :simulator &&
|
25
|
+
slice.supported_archs.include?('arm64')
|
26
|
+
end
|
27
|
+
|
28
|
+
original_arm_slice_identifier = xcframework.slices.find do |slice|
|
29
|
+
slice.platform == :ios && slice.supported_archs.include?('arm64')
|
30
|
+
end.identifier
|
31
|
+
|
32
|
+
patched_arm_slice_identifier = 'ios-arm64-simulator'
|
33
|
+
|
34
|
+
STDERR.puts "Will patch #{xcframework_path}: #{original_arm_slice_identifier} -> #{patched_arm_slice_identifier}"
|
35
|
+
|
36
|
+
plist = xcframework.plist
|
37
|
+
slice_plist_to_add = plist['AvailableLibraries'].find { |s| s['LibraryIdentifier'] == original_arm_slice_identifier }.dup
|
38
|
+
slice_plist_to_add['LibraryIdentifier'] = patched_arm_slice_identifier
|
39
|
+
slice_plist_to_add['SupportedArchitectures'] = ['arm64']
|
40
|
+
slice_plist_to_add['SupportedPlatformVariant'] = 'simulator'
|
41
|
+
plist['AvailableLibraries'] << slice_plist_to_add
|
42
|
+
|
43
|
+
FileUtils.rm_rf(xcframework_path.join(patched_arm_slice_identifier))
|
44
|
+
FileUtils.cp_r(xcframework_path.join(original_arm_slice_identifier), xcframework_path.join(patched_arm_slice_identifier))
|
45
|
+
|
46
|
+
Xcodeproj::Plist.write_to_path(plist, xcframework_path.join('Info.plist'))
|
47
|
+
|
48
|
+
xcframework = Pod::Xcode::XCFramework.open_xcframework(xcframework_path)
|
49
|
+
|
50
|
+
slice = xcframework.slices.find { |s| s.identifier == patched_arm_slice_identifier }
|
51
|
+
|
52
|
+
ArmPatcher.patch_arm_binary(slice)
|
53
|
+
ArmPatcher.cleanup_unused_archs(slice)
|
54
|
+
|
55
|
+
xcframework_path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cocoapods'
|
4
|
+
require 'cocoapods/xcode/xcframework'
|
5
|
+
|
6
|
+
module Pod
|
7
|
+
module Xcode
|
8
|
+
# open XCFramework
|
9
|
+
class XCFramework
|
10
|
+
def self.open_xcframework(xcframework_path)
|
11
|
+
if instance_method(:initialize).arity == 2
|
12
|
+
new(File.basename(xcframework_path), xcframework_path.realpath)
|
13
|
+
else
|
14
|
+
new(xcframework_path.realpath)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'xcframework_converter/creation'
|
4
|
+
require_relative 'xcframework_converter/patching'
|
3
5
|
require_relative 'xcframework_converter/version'
|
4
6
|
|
5
7
|
require 'cocoapods'
|
@@ -7,7 +9,7 @@ require 'cocoapods/xcode/xcframework'
|
|
7
9
|
require 'fileutils'
|
8
10
|
require 'xcodeproj'
|
9
11
|
|
10
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
11
13
|
|
12
14
|
# Converts a framework (static or dynamic) to XCFrameworks, adding an arm64 simulator patch.
|
13
15
|
# For more info:
|
@@ -19,6 +21,16 @@ module XCFrameworkConverter
|
|
19
21
|
installer.analysis_result.specifications.each do |spec|
|
20
22
|
next if spec.source && spec.local?
|
21
23
|
|
24
|
+
pod_path = installer.sandbox.pod_dir(Pod::Specification.root_name(spec.name))
|
25
|
+
|
26
|
+
xcframeworks_to_patch = spec.available_platforms.map do |platform|
|
27
|
+
consumer = Pod::Specification::Consumer.new(spec, platform)
|
28
|
+
consumer.vendored_frameworks.select { |f| File.extname(f) == '.xcframework' }
|
29
|
+
.map { |f| pod_path.join(f) }
|
30
|
+
end.flatten.uniq
|
31
|
+
|
32
|
+
patch_xcframeworks_if_needed(spec, xcframeworks_to_patch)
|
33
|
+
|
22
34
|
frameworks_to_convert = spec.available_platforms.map do |platform|
|
23
35
|
consumer = Pod::Specification::Consumer.new(spec, platform)
|
24
36
|
before_rename = consumer.vendored_frameworks.select { |f| File.extname(f) == '.framework' }
|
@@ -27,112 +39,36 @@ module XCFrameworkConverter
|
|
27
39
|
after_rename = before_rename.map { |f| Pathname.new(f).sub_ext('.xcframework').to_s }
|
28
40
|
proxy = Pod::Specification::DSL::PlatformProxy.new(spec, platform.symbolic_name)
|
29
41
|
proxy.vendored_frameworks = consumer.vendored_frameworks - before_rename + after_rename
|
30
|
-
before_rename
|
42
|
+
before_rename.map { |f| pod_path.join(f) }
|
31
43
|
end.flatten.uniq
|
32
44
|
|
33
|
-
|
34
|
-
|
35
|
-
pod_path = installer.sandbox.pod_dir(Pod::Specification.root_name(spec.name))
|
36
|
-
convert_xcframeworks_if_present(frameworks_to_convert.map { |f| pod_path.join(f) })
|
37
|
-
|
38
|
-
# some pods put these as a way to NOT support arm64 sim
|
39
|
-
# may stop working if a pod decides to put these in a platform proxy
|
40
|
-
spec.attributes_hash['pod_target_xcconfig']&.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
41
|
-
spec.attributes_hash['user_target_xcconfig']&.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
45
|
+
convert_xcframeworks_if_present(spec, frameworks_to_convert)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
|
-
def convert_xcframeworks_if_present(frameworks_to_convert)
|
49
|
+
def convert_xcframeworks_if_present(spec, frameworks_to_convert)
|
46
50
|
frameworks_to_convert.each do |path|
|
47
51
|
convert_framework_to_xcframework(path) if Dir.exist?(path)
|
48
52
|
end
|
53
|
+
remove_troublesome_xcconfig_items(spec) unless frameworks_to_convert.empty?
|
49
54
|
end
|
50
55
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def convert_framework_to_xcframework(path)
|
56
|
-
plist = Xcodeproj::Plist.read_from_path(plist_template_path)
|
57
|
-
xcframework_path = Pathname.new(path).sub_ext('.xcframework')
|
58
|
-
xcframework_path.mkdir
|
59
|
-
plist['AvailableLibraries'].each do |slice|
|
60
|
-
slice_path = xcframework_path.join(slice['LibraryIdentifier'])
|
61
|
-
slice_path.mkdir
|
62
|
-
slice['LibraryPath'] = File.basename(path)
|
63
|
-
FileUtils.cp_r(path, slice_path)
|
64
|
-
end
|
65
|
-
Xcodeproj::Plist.write_to_path(plist, xcframework_path.join('Info.plist'))
|
66
|
-
FileUtils.rm_rf(path)
|
67
|
-
final_framework = open_xcframework(xcframework_path)
|
68
|
-
final_framework.slices.each do |slice|
|
69
|
-
patch_arm_binary(slice) if slice.platform == :ios && slice.platform_variant == :simulator
|
70
|
-
cleanup_unused_archs(slice)
|
71
|
-
end
|
72
|
-
end
|
56
|
+
def patch_xcframeworks_if_needed(spec, xcframeworks)
|
57
|
+
patched = xcframeworks.map do |path|
|
58
|
+
next nil unless Dir.exist?(path)
|
73
59
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
else
|
78
|
-
Pod::Xcode::XCFramework.new(xcframework_path.realpath)
|
79
|
-
end
|
60
|
+
patch_xcframework(path)
|
61
|
+
end.compact
|
62
|
+
remove_troublesome_xcconfig_items(spec) unless patched.empty?
|
80
63
|
end
|
81
64
|
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
patch_arm_binary_static(slice)
|
90
|
-
end
|
91
|
-
|
92
|
-
slice.path.glob('**/arm64*.swiftinterface').each do |interface_file|
|
93
|
-
`sed -i '' -E 's/target arm64-apple-ios([0-9.]+) /target arm64-apple-ios\\1-simulator /g' "#{interface_file}"`
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def patch_arm_binary_dynamic(slice)
|
98
|
-
extracted_path = slice.path.join('arm64.dylib')
|
99
|
-
`xcrun lipo \"#{slice.binary_path}\" -thin arm64 -output \"#{extracted_path}\"`
|
100
|
-
|
101
|
-
file = MachO::MachOFile.new(extracted_path)
|
102
|
-
sdk_version = file[:LC_VERSION_MIN_IPHONEOS].first.version_string
|
103
|
-
`xcrun vtool -arch arm64 -set-build-version 7 #{sdk_version} #{sdk_version} -replace -output \"#{extracted_path}\" \"#{extracted_path}\"`
|
104
|
-
`xcrun lipo \"#{slice.binary_path}\" -replace arm64 \"#{extracted_path}\" -output \"#{slice.binary_path}\"`
|
105
|
-
extracted_path.rmtree
|
106
|
-
end
|
107
|
-
|
108
|
-
def arm2sim_path
|
109
|
-
Pathname.new(__FILE__).dirname.join('arm2sim.swift')
|
110
|
-
end
|
111
|
-
|
112
|
-
def patch_arm_binary_static(slice)
|
113
|
-
extracted_path = slice.path.join('arm64.a')
|
114
|
-
`xcrun lipo \"#{slice.binary_path}\" -thin arm64 -output \"#{extracted_path}\"`
|
115
|
-
extracted_path_dir = slice.path.join('arm64-objects')
|
116
|
-
extracted_path_dir.mkdir
|
117
|
-
`cd \"#{extracted_path_dir}\" ; ar x \"#{extracted_path}\"`
|
118
|
-
Dir[extracted_path_dir.join('*.o')].each do |object_file|
|
119
|
-
file = MachO::MachOFile.new(object_file)
|
120
|
-
sdk_version = file[:LC_VERSION_MIN_IPHONEOS].first.version_string.to_i
|
121
|
-
`xcrun swift \"#{arm2sim_path}\" \"#{object_file}\" \"#{sdk_version}\" \"#{sdk_version}\"`
|
122
|
-
end
|
123
|
-
`cd \"#{extracted_path_dir}\" ; ar crv \"#{extracted_path}\" *.o`
|
124
|
-
|
125
|
-
`xcrun lipo \"#{slice.binary_path}\" -replace arm64 \"#{extracted_path}\" -output \"#{slice.binary_path}\"`
|
126
|
-
extracted_path_dir.rmtree
|
127
|
-
extracted_path.rmtree
|
128
|
-
end
|
129
|
-
|
130
|
-
def cleanup_unused_archs(slice)
|
131
|
-
supported_archs = slice.supported_archs
|
132
|
-
unsupported_archs = `xcrun lipo \"#{slice.binary_path}\" -archs`.split - supported_archs
|
133
|
-
unsupported_archs.each do |arch|
|
134
|
-
`xcrun lipo \"#{slice.binary_path}\" -remove \"#{arch}\" -output \"#{slice.binary_path}\"`
|
135
|
-
end
|
65
|
+
def remove_troublesome_xcconfig_items(spec)
|
66
|
+
# some pods put these as a way to NOT support arm64 sim
|
67
|
+
# may stop working if a pod decides to put these in a platform proxy
|
68
|
+
spec.attributes_hash['pod_target_xcconfig']&.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
69
|
+
spec.attributes_hash['user_target_xcconfig']&.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
70
|
+
spec.attributes_hash['pod_target_xcconfig']&.delete('VALID_ARCHS[sdk=iphonesimulator*]')
|
71
|
+
spec.attributes_hash['user_target_xcconfig']&.delete('VALID_ARCHS[sdk=iphonesimulator*]')
|
136
72
|
end
|
137
73
|
end
|
138
74
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xcframework_converter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Makarov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cocoapods
|
@@ -63,7 +63,11 @@ files:
|
|
63
63
|
- bin/xcfconvert
|
64
64
|
- lib/arm2sim.swift
|
65
65
|
- lib/xcframework_converter.rb
|
66
|
+
- lib/xcframework_converter/arm_patcher.rb
|
67
|
+
- lib/xcframework_converter/creation.rb
|
68
|
+
- lib/xcframework_converter/patching.rb
|
66
69
|
- lib/xcframework_converter/version.rb
|
70
|
+
- lib/xcframework_converter/xcframework_ext.rb
|
67
71
|
- lib/xcframework_template.plist
|
68
72
|
homepage: https://github.com/igor-makarov/XCFrameworkConverter
|
69
73
|
licenses:
|