From 98aee404af46fbdf26634fd879219d70d6cebd1c Mon Sep 17 00:00:00 2001 From: Tommaso Piazza Date: Thu, 28 Jun 2018 12:03:20 +0200 Subject: [PATCH] Check the Mach headers when there is no Info.plist --- Carthage.xcodeproj/project.pbxproj | 8 + Source/CarthageKit/FrameworkExtensions.swift | 6 + Source/CarthageKit/MachHeader.swift | 146 +++++++++++++++++++ Source/CarthageKit/Project.swift | 21 ++- Tests/CarthageKitTests/MachHeaderSpec.swift | 27 ++++ Tests/CarthageKitTests/ProjectSpec.swift | 26 ++-- 6 files changed, 216 insertions(+), 18 deletions(-) create mode 100644 Source/CarthageKit/MachHeader.swift create mode 100644 Tests/CarthageKitTests/MachHeaderSpec.swift diff --git a/Carthage.xcodeproj/project.pbxproj b/Carthage.xcodeproj/project.pbxproj index a39ae0f9f7..d9f8c26c36 100644 --- a/Carthage.xcodeproj/project.pbxproj +++ b/Carthage.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 3B19041E1E4CFE4A00A866AD /* FakeOldObjc.framework in Resources */ = {isa = PBXBuildFile; fileRef = 3B19041C1E4CFE3900A866AD /* FakeOldObjc.framework */; }; 3B19041F1E4CFE4A00A866AD /* FakeOldSwift.framework in Resources */ = {isa = PBXBuildFile; fileRef = 3B19041D1E4CFE3900A866AD /* FakeOldSwift.framework */; }; 3BE5BD9A1E4E5A7B00DDDC45 /* SwiftVersionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE5BD981E4E58ED00DDDC45 /* SwiftVersionError.swift */; }; + 4396DC8F20E4E8A8002EE967 /* MachHeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4396DC8E20E4E8A8002EE967 /* MachHeaderSpec.swift */; }; + 43D7809520E4324E004BCDD6 /* MachHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4396DC8C20E3EF5C002EE967 /* MachHeader.swift */; }; 5482DAF11A3849D700197FB8 /* CopyFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5482DAF01A3849D700197FB8 /* CopyFrameworks.swift */; }; 54911EF31A1D34EC00FFAE5F /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54911EF21A1D34EC00FFAE5F /* Version.swift */; }; 549B47B11A4F1A34002498C7 /* ProjectSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549B47AF1A4F17FF002498C7 /* ProjectSpec.swift */; }; @@ -183,6 +185,8 @@ 3B19041C1E4CFE3900A866AD /* FakeOldObjc.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FakeOldObjc.framework; sourceTree = ""; }; 3B19041D1E4CFE3900A866AD /* FakeOldSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FakeOldSwift.framework; sourceTree = ""; }; 3BE5BD981E4E58ED00DDDC45 /* SwiftVersionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftVersionError.swift; sourceTree = ""; }; + 4396DC8C20E3EF5C002EE967 /* MachHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachHeader.swift; sourceTree = ""; }; + 4396DC8E20E4E8A8002EE967 /* MachHeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachHeaderSpec.swift; sourceTree = ""; }; 5482DAF01A3849D700197FB8 /* CopyFrameworks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyFrameworks.swift; sourceTree = ""; }; 54911EF21A1D34EC00FFAE5F /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; 5499CA961A2394B700783309 /* Components.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Components.plist; sourceTree = ""; }; @@ -532,6 +536,7 @@ 3BE5BD981E4E58ED00DDDC45 /* SwiftVersionError.swift */, D0DE893F1A0F2CB00030A3EC /* Version.swift */, F1CB3CFB1D63D05D00C9EB99 /* VersionFile.swift */, + 4396DC8C20E3EF5C002EE967 /* MachHeader.swift */, BE0292481E403355004FB579 /* XCDBLDExtensions.swift */, D01F8A5319EA2F1700643E7C /* Xcode.swift */, D0D1216F19E87B05005E4BAA /* Supporting Files */, @@ -573,6 +578,7 @@ B1F27D3D1E45382B002D4754 /* VersionFileSpec.swift */, D0DB09A319EA354200234B16 /* XcodeSpec.swift */, 21F11B461FE6787F009FB783 /* DB.swift */, + 4396DC8E20E4E8A8002EE967 /* MachHeaderSpec.swift */, D0D1217C19E87B05005E4BAA /* Supporting Files */, ); name = CarthageKitTests; @@ -844,6 +850,7 @@ D074EDC51A049283001DE082 /* FrameworkExtensions.swift in Sources */, CDE559291E12263A00ED7F5F /* BuildSettings.swift in Sources */, 88ED56D619ECE34900CBF5C4 /* Git.swift in Sources */, + 43D7809520E4324E004BCDD6 /* MachHeader.swift in Sources */, D0D1219219E88B8F005E4BAA /* GitHub.swift in Sources */, CD28C99D1E11846200322AF7 /* ProductType.swift in Sources */, CD43D9DA1F41640E00CD60F6 /* CarthageKitVersion.swift in Sources */, @@ -883,6 +890,7 @@ BFFA7A631E3AAFD200CB95A7 /* BinaryProjectSpec.swift in Sources */, D0C6E5741A57040B00A5E3E7 /* ArchiveSpec.swift in Sources */, 549B47B11A4F1A34002498C7 /* ProjectSpec.swift in Sources */, + 4396DC8F20E4E8A8002EE967 /* MachHeaderSpec.swift in Sources */, D01D82DD1A10B01D00F0DD94 /* ResolverSpec.swift in Sources */, BF3199C01E32E078007DC0D1 /* DependencySpec.swift in Sources */, CDA0B6CA1C468E67006C499C /* GitURLSpec.swift in Sources */, diff --git a/Source/CarthageKit/FrameworkExtensions.swift b/Source/CarthageKit/FrameworkExtensions.swift index ef0025e8fb..93c4198677 100644 --- a/Source/CarthageKit/FrameworkExtensions.swift +++ b/Source/CarthageKit/FrameworkExtensions.swift @@ -19,6 +19,12 @@ extension String { } } + /// Strips off a prefix string, if present. + internal func stripping(prefix: String) -> String { + guard hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } + /// Strips off a trailing string, if present. internal func stripping(suffix: String) -> String { if hasSuffix(suffix) { diff --git a/Source/CarthageKit/MachHeader.swift b/Source/CarthageKit/MachHeader.swift new file mode 100644 index 0000000000..511e4e650d --- /dev/null +++ b/Source/CarthageKit/MachHeader.swift @@ -0,0 +1,146 @@ +import Foundation +import MachO.loader +import ReactiveTask +import ReactiveSwift +import Result + +/// Represents a Mach header +/// +/// Provides a unified structure for +/// `MachO.loader.mach_header` and `MachO.loader.mach_header_64` + +struct MachHeader { + + enum Endianness { + case little + case big + } + + let magic: UInt32 + let cpuType: cpu_type_t + let cpuSubtype: cpu_type_t + let fileType: UInt32 + let ncmds: UInt32 + let sizeofcmds: UInt32 + let flags: UInt32 + let reserved: UInt32? + + var is64BitHeader: Bool { + + return magic == MH_MAGIC_64 || magic == MH_CIGAM_64 + } + + var is32BitHeader: Bool { + + return !is64BitHeader + } + + var endianess: Endianness { + + return magic == MH_CIGAM_64 || magic == MH_CIGAM ? .big : .little + } +} + +extension MachHeader { + + static let carthageSupportedFileTypes: Set = { + return Set([ + MH_OBJECT, // Carthage accepts static libraries + MH_BUNDLE, // Bundles https://github.com/ResearchKit/ResearchKit/blob/1.3.0/ResearchKit/Info.plist#L15-L16 + MH_DYLIB, // or dynamic shared libraries + ].map { UInt32($0) } + ) + }() +} + +extension MachHeader { + + /// Reads the Mach headers from a Mach-O file. + /// - Parameter url: The url of the Mach-O file + /// - Remark: Uses `objdump` to read the header and parse the output. + /// The output is composed of one or more sets of lines like the following: + /// + /// Mach header + /// magic cputype cpusubtype caps filetype ncmds sizeofcmds flags + /// 0xfeedfacf 16777223 3 0x00 1 8 1720 0x00002000 + /// + /// - See Also: [LLVM MachODump.cpp](https://llvm.org/viewvc/llvm-project/llvm/trunk/tools/llvm-objdump/MachODump.cpp?view=markup&pathrev=225383###see%C2%B7line%C2%B72745) + + static func headers(forMachOFileAtUrl url: URL) -> SignalProducer { + + // This is the command `otool -h` actually invokes + let task = Task("/usr/bin/xcrun", arguments: [ + "objdump", + "-macho", + "-private-header", + "-non-verbose", + url.resolvingSymlinksInPath().path + ] + ) + + return task.launch(standardInput: nil) + .ignoreTaskData() + .map { String(data: $0, encoding: .utf8) ?? "" } + .filter { !$0.isEmpty } + .flatMap(.merge) { (output: String) -> SignalProducer<(String, String), NoError> in + output.linesProducer.combinePrevious() + }.filterMap { (previousLine, currentLine) -> MachHeader? in + + let previousLineComponents = previousLine + .components(separatedBy: CharacterSet.whitespaces) + .filter { !$0.isEmpty } + let currentLineComponents = currentLine + .components(separatedBy: CharacterSet.whitespaces) + .filter { !$0.isEmpty } + + let strippedComponents = currentLineComponents + .map { $0.stripping(prefix: "0x")} + + let magicIdentifiers = [ + MH_MAGIC_64, + MH_CIGAM_64, + MH_MAGIC, + MH_CIGAM, + ].lazy + + guard previousLineComponents == [ + "magic", + "cputype", + "cpusubtype", + "caps", + "filetype", + "ncmds", + "sizeofcmds", + "flags" + ] + , !strippedComponents.isEmpty + , let magic = UInt32(strippedComponents.first!, radix:16) + , magicIdentifiers.first(where: { $0 == magic }) != nil else { + return nil + } + + guard + let cpuType = cpu_type_t(strippedComponents[1], radix: 10), + let cpuSubtype = cpu_subtype_t(strippedComponents[2], radix: 10), + let fileType = UInt32(strippedComponents[4], radix: 10), + let ncmds = UInt32(strippedComponents[5], radix: 10), + let sizeofcmds = UInt32(strippedComponents[6], radix: 10), + let flags = UInt32(strippedComponents[7], radix: 16) + else { + return nil + } + + return MachHeader( + magic: magic, + cpuType: cpuType, + cpuSubtype: cpuSubtype, + fileType: fileType, + ncmds: ncmds, + sizeofcmds: sizeofcmds, + flags: flags, + reserved: nil + ) + } + .mapError(CarthageError.taskError) + } +} diff --git a/Source/CarthageKit/Project.swift b/Source/CarthageKit/Project.swift index d80ca0244f..7309fdc741 100644 --- a/Source/CarthageKit/Project.swift +++ b/Source/CarthageKit/Project.swift @@ -1282,23 +1282,34 @@ internal func frameworksInDirectory(_ directoryURL: URL) -> SignalProducer()) { $0.insert($1.fileType); return } + .map { $0.count == 1 } + .single()? + .value ?? false } - } + } } /// Sends the URL to each dSYM found in the given directory diff --git a/Tests/CarthageKitTests/MachHeaderSpec.swift b/Tests/CarthageKitTests/MachHeaderSpec.swift new file mode 100644 index 0000000000..3bde1fdbd5 --- /dev/null +++ b/Tests/CarthageKitTests/MachHeaderSpec.swift @@ -0,0 +1,27 @@ +@testable import CarthageKit +import Foundation +import Nimble +import Quick +import ReactiveSwift +import Result + +class MachHeaderSpec: QuickSpec { + + override func spec() { + + describe("headers") { + it("should list all mach headers for a given Mach-O file") { + let directoryURL = Bundle(for: type(of: self)).url(forResource: "Alamofire.framework", withExtension: nil)! + + let result = CarthageKit + .MachHeader + .headers(forMachOFileAtUrl: directoryURL.appendingPathComponent("Alamofire")) + .collect() + .single() + + expect(result?.value?.count) == 36 + } + } + } + +} diff --git a/Tests/CarthageKitTests/ProjectSpec.swift b/Tests/CarthageKitTests/ProjectSpec.swift index 36053e12bb..2f36e721e9 100644 --- a/Tests/CarthageKitTests/ProjectSpec.swift +++ b/Tests/CarthageKitTests/ProjectSpec.swift @@ -18,7 +18,7 @@ class ProjectSpec: QuickSpec { let noSharedSchemesDirectoryURL = Bundle(for: type(of: self)).url(forResource: "NoSharedSchemesTest", withExtension: nil)! let noSharedSchemesBuildDirectoryURL = noSharedSchemesDirectoryURL.appendingPathComponent(Constants.binariesFolderPath) - + func build(directoryURL url: URL, platforms: Set = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { let project = Project(directoryURL: url) let result = project.buildCheckedOutDependenciesWithOptions(BuildOptions(configuration: "Debug", platforms: platforms, cacheBuilds: cacheBuilds), dependenciesToBuild: dependenciesToBuild) @@ -33,11 +33,11 @@ class ProjectSpec: QuickSpec { return result.value!.map { $0.name } } - + func buildDependencyTest(platforms: Set = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { return build(directoryURL: directoryURL, platforms: platforms, cacheBuilds: cacheBuilds, dependenciesToBuild: dependenciesToBuild) } - + func buildNoSharedSchemesTest(platforms: Set = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { return build(directoryURL: noSharedSchemesDirectoryURL, platforms: platforms, cacheBuilds: cacheBuilds, dependenciesToBuild: dependenciesToBuild) } @@ -202,22 +202,22 @@ class ProjectSpec: QuickSpec { expect(result2.filter { $0.contains("Mac") }) == ["TestFramework1_Mac"] expect(result2.filter { $0.contains("iOS") }) == ["TestFramework1_iOS"] } - + it("should create and read a version file for a project with no shared schemes") { let result = buildNoSharedSchemesTest(platforms: [.iOS]) expect(result) == ["TestFramework1_iOS"] let result2 = buildNoSharedSchemesTest(platforms: [.iOS]) expect(result2) == [] - + // TestFramework2 has no shared schemes, but invalidating its version file should result in its dependencies (TestFramework1) being rebuilt let framework2VersionFileURL = noSharedSchemesBuildDirectoryURL.appendingPathComponent(".TestFramework2.version", isDirectory: false) let framework2VersionFilePath = framework2VersionFileURL.path - + let json = try! String(contentsOf: framework2VersionFileURL, encoding: .utf8) let modifiedJson = json.replacingOccurrences(of: "\"commitish\" : \"v1.0\"", with: "\"commitish\" : \"v1.1\"") _ = try! modifiedJson.write(toFile: framework2VersionFilePath, atomically: true, encoding: .utf8) - + let result3 = buildNoSharedSchemesTest(platforms: [.iOS]) expect(result3) == ["TestFramework1_iOS"] } @@ -495,34 +495,34 @@ class ProjectSpec: QuickSpec { expect(result).notTo(beNil()) expect(result!.error).to(beNil()) expect(result!.value!).notTo(beNil()) - + let outdatedDependencies = result!.value!.reduce(into: [:], { (result, next) in result[next.0] = (next.1, next.2, next.3) }) // Github 1 has no updates available expect(outdatedDependencies[github1]).to(beNil()) - + // Github 2 is currently at 1.0.0, can be updated to the latest version which is 2.0.0 // Github 2 has no constraint in the Cartfile expect(outdatedDependencies[github2]!.0) == PinnedVersion("v1.0.0") expect(outdatedDependencies[github2]!.1) == PinnedVersion("v2.0.0") expect(outdatedDependencies[github2]!.2) == PinnedVersion("v2.0.0") - + // Github 3 is currently at 2.0.0, latest is 2.0.1, to which it can be updated // Github 3 has a constraint in the Cartfile expect(outdatedDependencies[github3]!.0) == PinnedVersion("v2.0.0") expect(outdatedDependencies[github3]!.1) == PinnedVersion("v2.0.1") expect(outdatedDependencies[github3]!.2) == PinnedVersion("v2.0.1") - + // Github 4 is currently at 2.0.0, latest is 3.0.0, but it can only be updated to 2.0.1 expect(outdatedDependencies[github4]!.0) == PinnedVersion("v2.0.0") expect(outdatedDependencies[github4]!.1) == PinnedVersion("v2.0.1") expect(outdatedDependencies[github4]!.2) == PinnedVersion("v3.0.0") - + // Github 5 is pinned to a branch and is already at the most recent commit, so it should not be displayed expect(outdatedDependencies[github5]).to(beNil()) - + // Github 6 is pinned ot a branch which has new commits, so it should be displayed expect(outdatedDependencies[github6]!.0) == PinnedVersion(currentSHA) expect(outdatedDependencies[github6]!.1) == PinnedVersion(nextSHA)