Skip to content

Commit

Permalink
Check the Mach headers when there is no Info.plist
Browse files Browse the repository at this point in the history
  • Loading branch information
tmspzz committed Jun 29, 2018
1 parent e9a10ee commit 98aee40
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 18 deletions.
8 changes: 8 additions & 0 deletions Carthage.xcodeproj/project.pbxproj
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -183,6 +185,8 @@
3B19041C1E4CFE3900A866AD /* FakeOldObjc.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FakeOldObjc.framework; sourceTree = "<group>"; };
3B19041D1E4CFE3900A866AD /* FakeOldSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FakeOldSwift.framework; sourceTree = "<group>"; };
3BE5BD981E4E58ED00DDDC45 /* SwiftVersionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftVersionError.swift; sourceTree = "<group>"; };
4396DC8C20E3EF5C002EE967 /* MachHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachHeader.swift; sourceTree = "<group>"; };
4396DC8E20E4E8A8002EE967 /* MachHeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachHeaderSpec.swift; sourceTree = "<group>"; };
5482DAF01A3849D700197FB8 /* CopyFrameworks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyFrameworks.swift; sourceTree = "<group>"; };
54911EF21A1D34EC00FFAE5F /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = "<group>"; };
5499CA961A2394B700783309 /* Components.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Components.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -573,6 +578,7 @@
B1F27D3D1E45382B002D4754 /* VersionFileSpec.swift */,
D0DB09A319EA354200234B16 /* XcodeSpec.swift */,
21F11B461FE6787F009FB783 /* DB.swift */,
4396DC8E20E4E8A8002EE967 /* MachHeaderSpec.swift */,
D0D1217C19E87B05005E4BAA /* Supporting Files */,
);
name = CarthageKitTests;
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
6 changes: 6 additions & 0 deletions Source/CarthageKit/FrameworkExtensions.swift
Expand Up @@ -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) {
Expand Down
146 changes: 146 additions & 0 deletions 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<UInt32> = {
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<MachHeader, CarthageError> {

// 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)
}
}
21 changes: 16 additions & 5 deletions Source/CarthageKit/Project.swift
Expand Up @@ -1282,23 +1282,34 @@ internal func frameworksInDirectory(_ directoryURL: URL) -> SignalProducer<URL,
return filesInDirectory(directoryURL, kUTTypeFramework as String)
.filter { !$0.pathComponents.contains("__MACOSX") }
.filter { url in

// Skip nested frameworks
let frameworksInURL = url.pathComponents.filter { pathComponent in
return (pathComponent as NSString).pathExtension == "framework"
}
return frameworksInURL.count == 1
}.filter { url in

let packageType: PackageType? = Bundle(url: url)?.packageType
// For reasons of speed and the fact that CLI-output structures can change,
// first try the safer method of reading the ‘Info.plist’ from the Framework’s bundle.
let bundle = Bundle(url: url)
let packageType: PackageType? = bundle?.packageType

switch packageType {
case .framework?, .bundle?:
return true
default:
return false
// In case no Info.plist exists check the Mach-O fileType
guard let executableURL = bundle?.executableURL else {
return false
}

return MachHeader.headers(forMachOFileAtUrl: executableURL)
.filter { MachHeader.carthageSupportedFileTypes.contains($0.fileType) }
.reduce(into: Set<UInt32>()) { $0.insert($1.fileType); return }
.map { $0.count == 1 }
.single()?
.value ?? false
}
}
}
}

/// Sends the URL to each dSYM found in the given directory
Expand Down
27 changes: 27 additions & 0 deletions 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
}
}
}

}
26 changes: 13 additions & 13 deletions Tests/CarthageKitTests/ProjectSpec.swift
Expand Up @@ -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<Platform> = [], 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)
Expand All @@ -33,11 +33,11 @@ class ProjectSpec: QuickSpec {

return result.value!.map { $0.name }
}

func buildDependencyTest(platforms: Set<Platform> = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] {
return build(directoryURL: directoryURL, platforms: platforms, cacheBuilds: cacheBuilds, dependenciesToBuild: dependenciesToBuild)
}

func buildNoSharedSchemesTest(platforms: Set<Platform> = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] {
return build(directoryURL: noSharedSchemesDirectoryURL, platforms: platforms, cacheBuilds: cacheBuilds, dependenciesToBuild: dependenciesToBuild)
}
Expand Down Expand Up @@ -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"]
}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 98aee40

Please sign in to comment.