xcframework_converter 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 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: []