xcframework_converter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 261230f022e462a9840a009590e212c543833902b44795f80476971ff828dae7
4
+ data.tar.gz: 5a383fe7e654d79ea3d292a6369e4ba1d1b177c03f8967c9d6ec4fca2d942563
5
+ SHA512:
6
+ metadata.gz: c284b721156bc30b0e818d8048513c023c2e931cb659d7211959f356c08f36f133a61494078edb35b24b490ccd6b15b5c71d7c61b1a42b6da14adab664517700
7
+ data.tar.gz: 2be83f317ea5fa0e2e108ac81cbfbfaf1095c5738559555fafabcf0f215efaf1c08308f6f01b6fd337b1a190bc319367c7634e6a00a73a0ccbf029edd7471683
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # XcframeworkConverter
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/xcframework_converter`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'xcframework_converter'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install xcframework_converter
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/xcframework_converter. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/xcframework_converter/blob/master/CODE_OF_CONDUCT.md).
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
+
41
+ ## Code of Conduct
42
+
43
+ Everyone interacting in the XcframeworkConverter project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/xcframework_converter/blob/master/CODE_OF_CONDUCT.md).
data/bin/xcfconvert ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'xcframework_converter'
6
+
7
+ if ARGV.empty?
8
+ warn 'Usage: xcfconvert <path/to/Framework.framework>'
9
+ exit 1
10
+ end
11
+
12
+ XCFrameworkConverter.convert_framework_to_xcframework(Pathname.new(ARGV[0]).realpath)
data/lib/arm2sim.swift ADDED
@@ -0,0 +1,166 @@
1
+ import Foundation
2
+ import MachO
3
+
4
+ // support checking for Mach-O `cmd` and `cmdsize` properties
5
+ extension Data {
6
+ var loadCommand: UInt32 {
7
+ let lc: load_command = withUnsafeBytes { $0.load(as: load_command.self) }
8
+ return lc.cmd
9
+ }
10
+
11
+ var commandSize: Int {
12
+ let lc: load_command = withUnsafeBytes { $0.load(as: load_command.self) }
13
+ return Int(lc.cmdsize)
14
+ }
15
+
16
+ func asStruct<T>(fromByteOffset offset: Int = 0) -> T {
17
+ return withUnsafeBytes { $0.load(fromByteOffset: offset, as: T.self) }
18
+ }
19
+ }
20
+
21
+ extension Array where Element == Data {
22
+ func merge() -> Data {
23
+ return reduce(into: Data()) { $0.append($1) }
24
+ }
25
+ }
26
+
27
+ // support peeking at Data contents
28
+ extension FileHandle {
29
+ func peek(upToCount count: Int) throws -> Data? {
30
+ // persist the current offset, since `upToCount` doesn't guarantee all bytes will be read
31
+ let originalOffset = offsetInFile
32
+ let data = try read(upToCount: count)
33
+ try seek(toOffset: originalOffset)
34
+ return data
35
+ }
36
+ }
37
+
38
+ enum Transmogrifier {
39
+ private static func readBinary(atPath path: String) -> (Data, [Data], Data) {
40
+ guard let handle = FileHandle(forReadingAtPath: path) else {
41
+ fatalError("Cannot open a handle for the file at \(path). Aborting.")
42
+ }
43
+
44
+ // chop up the file into a relevant number of segments
45
+ let headerData = try! handle.read(upToCount: MemoryLayout<mach_header_64>.stride)!
46
+
47
+ let header: mach_header_64 = headerData.asStruct()
48
+ if header.magic != MH_MAGIC_64 || header.cputype != CPU_TYPE_ARM64 {
49
+ fatalError("The file is not a correct arm64 binary. Try thinning (via lipo) or unarchiving (via ar) first.")
50
+ }
51
+
52
+ let loadCommandsData: [Data] = (0..<header.ncmds).map { _ in
53
+ let loadCommandPeekData = try! handle.peek(upToCount: MemoryLayout<load_command>.stride)
54
+ return try! handle.read(upToCount: Int(loadCommandPeekData!.commandSize))!
55
+ }
56
+
57
+ let programData = try! handle.readToEnd()!
58
+
59
+ try! handle.close()
60
+
61
+ return (headerData, loadCommandsData, programData)
62
+ }
63
+
64
+ private static func updateSegment64(_ data: Data, _ offset: UInt32) -> Data {
65
+ // decode both the segment_command_64 and the subsequent section_64s
66
+ var segment: segment_command_64 = data.asStruct()
67
+
68
+ let sections: [section_64] = (0..<Int(segment.nsects)).map { index in
69
+ let offset = MemoryLayout<segment_command_64>.stride + index * MemoryLayout<section_64>.stride
70
+ return data.asStruct(fromByteOffset: offset)
71
+ }
72
+
73
+ // shift segment information by the offset
74
+ segment.fileoff += UInt64(offset)
75
+ segment.filesize += UInt64(offset)
76
+ segment.vmsize += UInt64(offset)
77
+
78
+ let offsetSections = sections.map { section -> section_64 in
79
+ var section = section
80
+ section.offset += UInt32(offset)
81
+ section.reloff += section.reloff > 0 ? UInt32(offset) : 0
82
+ return section
83
+ }
84
+
85
+ var datas = [Data]()
86
+ datas.append(Data(bytes: &segment, count: MemoryLayout<segment_command_64>.stride))
87
+ datas.append(contentsOf: offsetSections.map { section in
88
+ var section = section
89
+ return Data(bytes: &section, count: MemoryLayout<section_64>.stride)
90
+ })
91
+
92
+ return datas.merge()
93
+ }
94
+
95
+ private static func updateVersionMin(_ data: Data, _ offset: UInt32, minos: UInt32, sdk: UInt32) -> Data {
96
+ var command = build_version_command(cmd: UInt32(LC_BUILD_VERSION),
97
+ cmdsize: UInt32(MemoryLayout<build_version_command>.stride),
98
+ platform: UInt32(PLATFORM_IOSSIMULATOR),
99
+ minos: minos << 16 | 0 << 8 | 0,
100
+ sdk: sdk << 16 | 0 << 8 | 0,
101
+ ntools: 0)
102
+
103
+ return Data(bytes: &command, count: MemoryLayout<build_version_command>.stride)
104
+ }
105
+
106
+ private static func updateDataInCode(_ data: Data, _ offset: UInt32) -> Data {
107
+ var command: linkedit_data_command = data.asStruct()
108
+ command.dataoff += offset
109
+ return Data(bytes: &command, count: data.commandSize)
110
+ }
111
+
112
+ private static func updateSymTab(_ data: Data, _ offset: UInt32) -> Data {
113
+ var command: symtab_command = data.asStruct()
114
+ command.stroff += offset
115
+ command.symoff += offset
116
+ return Data(bytes: &command, count: data.commandSize)
117
+ }
118
+
119
+ static func processBinary(atPath path: String, minos: UInt32 = 13, sdk: UInt32 = 13) {
120
+ guard CommandLine.arguments.count > 1 else {
121
+ fatalError("Please add a path to command!")
122
+ }
123
+ let (headerData, loadCommandsData, programData) = readBinary(atPath: path)
124
+
125
+ // `offset` is kind of a magic number here, since we know that's the only meaningful change to binary size
126
+ // having a dynamic `offset` requires two passes over the load commands and is left as an exercise to the reader
127
+ let offset = UInt32(abs(MemoryLayout<build_version_command>.stride - MemoryLayout<version_min_command>.stride))
128
+
129
+ let editedCommandsData = loadCommandsData
130
+ .map { (lc) -> Data in
131
+ switch Int32(lc.loadCommand) {
132
+ case LC_SEGMENT_64:
133
+ return updateSegment64(lc, offset)
134
+ case LC_VERSION_MIN_IPHONEOS:
135
+ return updateVersionMin(lc, offset, minos: minos, sdk: sdk)
136
+ case LC_DATA_IN_CODE, LC_LINKER_OPTIMIZATION_HINT:
137
+ return updateDataInCode(lc, offset)
138
+ case LC_SYMTAB:
139
+ return updateSymTab(lc, offset)
140
+ case LC_BUILD_VERSION:
141
+ fatalError("This arm64 binary already contains an LC_BUILD_VERSION load command!")
142
+ default:
143
+ return lc
144
+ }
145
+ }
146
+ .merge()
147
+
148
+ var header: mach_header_64 = headerData.asStruct()
149
+ header.sizeofcmds = UInt32(editedCommandsData.count)
150
+
151
+ // reassemble the binary
152
+ let reworkedData = [
153
+ Data(bytes: &header, count: MemoryLayout<mach_header_64>.stride),
154
+ editedCommandsData,
155
+ programData
156
+ ].merge()
157
+
158
+ // save back to disk
159
+ try! reworkedData.write(to: URL(fileURLWithPath: path))
160
+ }
161
+ }
162
+
163
+ let binaryPath = CommandLine.arguments[1]
164
+ let minos = UInt32(CommandLine.arguments[2]) ?? 13
165
+ let sdk = UInt32(CommandLine.arguments[3]) ?? 13
166
+ Transmogrifier.processBinary(atPath: binaryPath, minos: minos, sdk: sdk)
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'xcframework_converter/version'
4
+
5
+ require 'cocoapods'
6
+ require 'cocoapods/xcode/xcframework'
7
+ require 'fileutils'
8
+ require 'xcodeproj'
9
+
10
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
11
+
12
+ # Converts a framework (static or dynamic) to XCFrameworks, adding an arm64 simulator patch.
13
+ # For more info:
14
+ # static libs: https://bogo.wtf/arm64-to-sim.html
15
+ # dylibs: https://bogo.wtf/arm64-to-sim-dylibs.html
16
+ module XCFrameworkConverter
17
+ class << self
18
+ def convert_frameworks_to_xcframeworks!(installer)
19
+ installer.analysis_result.specifications.each do |spec|
20
+ next unless spec.attributes_hash['vendored_frameworks']
21
+
22
+ frameworks = Array(spec.attributes_hash['vendored_frameworks'])
23
+ unconverted_frameworks = frameworks.select { |f| File.extname(f) == '.framework' }
24
+ next if unconverted_frameworks.empty?
25
+ next if spec.local?
26
+
27
+ pod_path = installer.sandbox.pod_dir(Pod::Specification.root_name(spec.name))
28
+ convert_xcframeworks_if_present(pod_path)
29
+
30
+ converted_frameworks = unconverted_frameworks.map do |path|
31
+ Pathname.new(path).sub_ext('.xcframework').to_s
32
+ end
33
+ spec.attributes_hash['vendored_frameworks'] = frameworks - unconverted_frameworks + converted_frameworks
34
+ # some pods put these as a way to NOT support arm64 sim
35
+ spec.attributes_hash['pod_target_xcconfig']&.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
36
+ spec.attributes_hash['user_target_xcconfig']&.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
37
+ end
38
+ end
39
+
40
+ def convert_xcframeworks_if_present(pod_path)
41
+ unconverted_paths = Dir[pod_path.join('**/*.framework')] - Dir[pod_path.join('**/*.xcframework/**/*')]
42
+ unconverted_paths.each do |path|
43
+ convert_framework_to_xcframework(path)
44
+ end
45
+ end
46
+
47
+ def plist_template_path
48
+ Pathname.new(__FILE__).dirname.join('xcframework_template.plist')
49
+ end
50
+
51
+ def convert_framework_to_xcframework(path)
52
+ plist = Xcodeproj::Plist.read_from_path(plist_template_path)
53
+ xcframework_path = Pathname.new(path).sub_ext('.xcframework')
54
+ xcframework_path.mkdir
55
+ plist['AvailableLibraries'].each do |slice|
56
+ slice_path = xcframework_path.join(slice['LibraryIdentifier'])
57
+ slice_path.mkdir
58
+ slice['LibraryPath'] = File.basename(path)
59
+ FileUtils.cp_r(path, slice_path)
60
+ end
61
+ Xcodeproj::Plist.write_to_path(plist, xcframework_path.join('Info.plist'))
62
+ FileUtils.rm_rf(path)
63
+ final_framework = Pod::Xcode::XCFramework.new(xcframework_path.realpath)
64
+ final_framework.slices.each do |slice|
65
+ patch_arm_binary(slice) if slice.platform == :ios && slice.platform_variant == :simulator
66
+ cleanup_unused_archs(slice)
67
+ end
68
+ end
69
+
70
+ def patch_arm_binary(slice)
71
+ require 'macho'
72
+
73
+ case slice.build_type.linkage
74
+ when :dynamic
75
+ patch_arm_binary_dynamic(slice)
76
+ when :static
77
+ patch_arm_binary_static(slice)
78
+ end
79
+ end
80
+
81
+ def patch_arm_binary_dynamic(slice)
82
+ extracted_path = slice.path.join('arm64.dylib')
83
+ `xcrun lipo \"#{slice.binary_path}\" -thin arm64 -output \"#{extracted_path}\"`
84
+
85
+ file = MachO::MachOFile.new(extracted_path)
86
+ sdk_version = file[:LC_VERSION_MIN_IPHONEOS].first.version_string
87
+ `xcrun vtool -arch arm64 -set-build-version 7 #{sdk_version} #{sdk_version} -replace -output \"#{extracted_path}\" \"#{extracted_path}\"`
88
+ `xcrun lipo \"#{slice.binary_path}\" -replace arm64 \"#{extracted_path}\" -output \"#{slice.binary_path}\"`
89
+ extracted_path.rmtree
90
+ end
91
+
92
+ def arm2sim_path
93
+ Pathname.new(__FILE__).dirname.join('arm2sim.swift')
94
+ end
95
+
96
+ def patch_arm_binary_static(slice)
97
+ extracted_path = slice.path.join('arm64.a')
98
+ `xcrun lipo \"#{slice.binary_path}\" -thin arm64 -output \"#{extracted_path}\"`
99
+ extracted_path_dir = slice.path.join('arm64-objects')
100
+ extracted_path_dir.mkdir
101
+ `cd \"#{extracted_path_dir}\" ; ar x \"#{extracted_path}\"`
102
+ Dir[extracted_path_dir.join('*.o')].each do |object_file|
103
+ file = MachO::MachOFile.new(object_file)
104
+ sdk_version = file[:LC_VERSION_MIN_IPHONEOS].first.version_string.to_i
105
+ `xcrun swift \"#{arm2sim_path}\" \"#{object_file}\" \"#{sdk_version}\" \"#{sdk_version}\"`
106
+ end
107
+ `cd \"#{extracted_path_dir}\" ; ar crv \"#{extracted_path}\" *.o`
108
+
109
+ `xcrun lipo \"#{slice.binary_path}\" -replace arm64 \"#{extracted_path}\" -output \"#{slice.binary_path}\"`
110
+ extracted_path_dir.rmtree
111
+ extracted_path.rmtree
112
+ end
113
+
114
+ def cleanup_unused_archs(slice)
115
+ supported_archs = slice.supported_archs
116
+ unsupported_archs = `xcrun lipo \"#{slice.binary_path}\" -archs`.split - supported_archs
117
+ unsupported_archs.each do |arch|
118
+ `xcrun lipo \"#{slice.binary_path}\" -remove \"#{arch}\" -output \"#{slice.binary_path}\"`
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XCFrameworkConverter
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,41 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>AvailableLibraries</key>
6
+ <array>
7
+ <dict>
8
+ <key>LibraryIdentifier</key>
9
+ <string>ios-arm64_x86_64-simulator</string>
10
+ <key>LibraryPath</key>
11
+ <string>Placeholder.framework</string>
12
+ <key>SupportedArchitectures</key>
13
+ <array>
14
+ <string>arm64</string>
15
+ <string>x86_64</string>
16
+ </array>
17
+ <key>SupportedPlatform</key>
18
+ <string>ios</string>
19
+ <key>SupportedPlatformVariant</key>
20
+ <string>simulator</string>
21
+ </dict>
22
+ <dict>
23
+ <key>LibraryIdentifier</key>
24
+ <string>ios-arm64_armv7</string>
25
+ <key>LibraryPath</key>
26
+ <string>Placeholder.framework</string>
27
+ <key>SupportedArchitectures</key>
28
+ <array>
29
+ <string>arm64</string>
30
+ <string>armv7</string>
31
+ </array>
32
+ <key>SupportedPlatform</key>
33
+ <string>ios</string>
34
+ </dict>
35
+ </array>
36
+ <key>CFBundlePackageType</key>
37
+ <string>XFWK</string>
38
+ <key>XCFrameworkFormatVersion</key>
39
+ <string>1.0</string>
40
+ </dict>
41
+ </plist>
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xcframework_converter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Igor Makarov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-07-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cocoapods
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.10.0
20
+ - - "~>"
21
+ - !ruby/object:Gem::Version
22
+ version: '1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.10.0
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1'
33
+ - !ruby/object:Gem::Dependency
34
+ name: xcodeproj
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.20.0
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '1'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 1.20.0
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1'
53
+ description: Convert an ancient .framework (dynamic or static) to an .xcframework.
54
+ Add an arm64 Simulator patch.
55
+ email:
56
+ - igormaka@gmail.com
57
+ executables:
58
+ - xcfconvert
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - bin/xcfconvert
64
+ - lib/arm2sim.swift
65
+ - lib/xcframework_converter.rb
66
+ - lib/xcframework_converter/version.rb
67
+ - lib/xcframework_template.plist
68
+ homepage: https://github.com/igor-makarov/XCFrameworkConverter
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/igor-makarov/XCFrameworkConverter
73
+ source_code_uri: https://github.com/igor-makarov/XCFrameworkConverter
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 2.4.0
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.1.4
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Convert an ancient .framework to an .xcframework.
93
+ test_files: []