Skip to content

Commit

Permalink
Merge pull request #45 from veracode/missing_dependency_tracking
Browse files Browse the repository at this point in the history
Support Framework Build Phase, Rework Dependency Mapping
  • Loading branch information
NinjaLikesCheez authored Dec 1, 2023
2 parents 783daa5 + 50de129 commit 1171006
Show file tree
Hide file tree
Showing 64 changed files with 3,121 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ output/
*.bc
*.dia
_build/
**/.build/*
**/.build/
8 changes: 4 additions & 4 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ excluded:
- GenIRLogging/.build/
- TestAssets/

disabled_rules:
- todo

line_length:
warning: 150
warning: 200
ignores_comments: true

disabled_rules:
- todo
18 changes: 12 additions & 6 deletions PBXProjParser/Sources/PBXProjParser/Models/PBXBuildFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,39 @@
import Foundation

public class PBXBuildFile: PBXObject {
#if FULL_PBX_PARSING
public let productRef: String?
public let fileRef: String?

#if FULL_PBX_PARSING
public let platformFilter: String?
public let platformFilters: [String]?
public let productRef: String?
public let settings: [String: Any]?
#endif

private enum CodingKeys: String, CodingKey {
case productRef
case fileRef

#if FULL_PBX_PARSING
case platformFilter
case platformFilters
case productRef
case settings
#endif
}

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

productRef = try container.decodeIfPresent(String.self, forKey: .productRef)
fileRef = try container.decodeIfPresent(String.self, forKey: .fileRef)

#if FULL_PBX_PARSING
platformFilter = try container.decodeIfPresent(String.self, forKey: .platformFilter)
platformFilters = try container.decodeIfPresent([String].self, forKey: .platformFilters)
productRef = try container.decodeIfPresent(String.self, forKey: .productRef)
settings = try container.decodeIfPresent([String: Any].self, forKey: .settings)
#endif

try super.init(from: decoder)
}
#endif
}

public class PBXBuildRule: PBXObject {}
14 changes: 9 additions & 5 deletions PBXProjParser/Sources/PBXProjParser/Models/PBXBuildPhase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,39 @@
import Foundation

public class PBXBuildPhase: PBXObject {
public let files: [String]
#if FULL_PBX_PARSING
public let alwaysOutOfDate: String?
public let buildActionMask: UInt32
public let files: [String]
public let runOnlyForDeploymentPostprocessing: Int
#endif

private enum CodingKeys: String, CodingKey {
case files
#if FULL_PBX_PARSING
case alwaysOutOfDate
case buildActionMask
case files
case runOnlyForDeploymentPostprocessing
#endif
}

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

#if FULL_PBX_PARSING
alwaysOutOfDate = try container.decodeIfPresent(String.self, forKey: .alwaysOutOfDate)

let mask = try container.decode(String.self, forKey: .buildActionMask)
buildActionMask = UInt32(mask) ?? 0

files = try container.decode([String].self, forKey: .files)

let flag = try container.decode(String.self, forKey: .runOnlyForDeploymentPostprocessing)
runOnlyForDeploymentPostprocessing = Int(flag) ?? 0
#endif

files = try container.decodeIfPresent([String].self, forKey: .files) ?? []

try super.init(from: decoder)
}
#endif
}

public class PBXCopyFilesBuildPhase: PBXBuildPhase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,39 @@ import Foundation
public class PBXFileReference: PBXObject {
#if FULL_PBX_PARSING
public let fileEncoding: String?
public let explicitFileType: String?
public let includeInIndex: String?
public let lastKnownFileType: String?
public let name: String?
public let sourceTree: String
#endif
public let explicitFileType: String?
public let path: String

private enum CodingKeys: String, CodingKey {
#if FULL_PBX_PARSING
case fileEncoding
case explicitFileType
case includeInIndex
case lastKnownFileType
case name
case sourceTree
#endif
case explicitFileType
case path
}

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

explicitFileType = try container.decodeIfPresent(String.self, forKey: .explicitFileType)
path = try container.decode(String.self, forKey: .path)

#if FULL_PBX_PARSING
fileEncoding = try container.decodeIfPresent(String.self, forKey: .fileEncoding)
explicitFileType = try container.decodeIfPresent(String.self, forKey: .explicitFileType)
includeInIndex = try container.decodeIfPresent(String.self, forKey: .includeInIndex)
lastKnownFileType = try container.decodeIfPresent(String.self, forKey: .lastKnownFileType)
name = try container.decodeIfPresent(String.self, forKey: .name)
sourceTree = try container.decode(String.self, forKey: .sourceTree)
#endif
path = try container.decode(String.self, forKey: .path)

try super.init(from: decoder)
}
Expand Down
9 changes: 6 additions & 3 deletions PBXProjParser/Sources/PBXProjParser/Models/PBXTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ public class PBXLegacyTarget: PBXTarget {}

public class PBXNativeTarget: PBXTarget {
#if FULL_PBX_PARSING
public let buildPhases: [String]
public let productInstallPath: String?
#endif
public let buildPhases: [String]
public let productType: String?
public let productReference: String?
public let packageProductDependencies: [String]
Expand All @@ -74,22 +74,25 @@ public class PBXNativeTarget: PBXTarget {
public enum TargetDependency {
case native(PBXNativeTarget)
case package(XCSwiftPackageProductDependency)
case externalProjectFramework(String)

public var name: String {
switch self {
case .native(let target):
return target.name
case .package(let package):
return package.productName
case .externalProjectFramework(let filename):
return (filename as NSString).deletingPathExtension
}
}
}

private enum CodingKeys: String, CodingKey {
#if FULL_PBX_PARSING
case buildPhases
case productInstallPath
#endif
case buildPhases
case productType
case productReference
case packageProductDependencies
Expand All @@ -99,9 +102,9 @@ public class PBXNativeTarget: PBXTarget {
let container = try decoder.container(keyedBy: CodingKeys.self)

#if FULL_PBX_PARSING
buildPhases = try container.decode([String].self, forKey: .buildPhases)
productInstallPath = try container.decodeIfPresent(String.self, forKey: .productInstallPath)
#endif
buildPhases = try container.decodeIfPresent([String].self, forKey: .buildPhases) ?? []
productType = try container.decodeIfPresent(String.self, forKey: .productType)
productReference = try container.decodeIfPresent(String.self, forKey: .productReference)
packageProductDependencies = try container.decodeIfPresent([String].self, forKey: .packageProductDependencies) ?? []
Expand Down
13 changes: 9 additions & 4 deletions PBXProjParser/Sources/PBXProjParser/ProjectParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,16 @@ public struct ProjectParser {
return target.targetDependencies
.values
.map { dependency in
if case .native(let native) = dependency, let path = project.path(for: native) {
return path
switch dependency {
case .native(let target):
if let path = project.path(for: target) {
return path
}

fallthrough
default:
return dependency.name
}

return dependency.name
}
}

Expand Down
82 changes: 73 additions & 9 deletions PBXProjParser/Sources/PBXProjParser/XcodeProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,9 @@ public struct XcodeProject {

packages = model.objects(of: .swiftPackageProductDependency, as: XCSwiftPackageProductDependency.self)

// First pass - get all the direct dependencies
// get all the direct dependencies
targets.forEach { determineDirectDependencies($0) }

// Second pass - get all the transitive dependencies
targets.forEach { determineTransitiveDependencies($0) }

targets.forEach { target in
logger.debug("target: \(target.name). Dependencies: \(target.targetDependencies.map { $0.1.name })")
}
Expand Down Expand Up @@ -104,6 +101,35 @@ public struct XcodeProject {
target.packageProductDependencies
.compactMap { model.object(forKey: $0, as: XCSwiftPackageProductDependency.self) }
.forEach { target.add(dependency: .package($0)) }

// Calculate dependencies from "Embed Frameworks" copy files build phase
let embeddedFrameworks = determineEmbeddedFrameworksDependencies(target, with: model)

// Calculate the dependencies from "Link Binary with Library" build phase
let linkLibraries = determineBuildPhaseFrameworkDependencies(target, with: model)

let buildFiles = embeddedFrameworks + linkLibraries

// Now, we have two potential targets - file & package dependencies.
// File dependencies will likely have a reference in another Xcode Project. We might not have seen said project yet, so we need to offload discovery until after we've parsed all projects...
// Package dependencies will be a swift package - those we can handle easily :)

// ONE: package dependencies - they are the easiest
buildFiles
.compactMap { $0.productRef }
.compactMap { model.object(forKey: $0, as: XCSwiftPackageProductDependency.self) }
.forEach { target.add(dependency: .package($0)) }

// TWO: Resolve dependencies to... a thing that refers to something in the other project
let fileReferences = buildFiles
.compactMap { $0.fileRef }
.compactMap { model.object(forKey: $0, as: PBXFileReference.self) }

fileReferences
.filter { $0.explicitFileType == "wrapper.framework" }
.compactMap { $0.path } // TODO: do we want to last path component the path here? Need to figure out matching...
.filter { !$0.contains("System/Library/Frameworks/")} // System frameworks will contain this path
.forEach { target.add(dependency: .externalProjectFramework($0)) }
}

/// Determines transitive dependencies by looping through direct dependencies and finding the items they depend on
Expand All @@ -125,14 +151,18 @@ public struct XcodeProject {

seen.insert(dependency.name)

if case .native(let native) = dependency {
logger.debug("Adding native dependency: \(dependency.name), deps: \(native.targetDependencies.map { $0.0 })")
targetDependencies.append(contentsOf: native.targetDependencies.map { $0.1 })
native.targetDependencies.forEach { target.add(dependency: $0.1) }
} else {
switch dependency {
case .native(let nativeTarget):
logger.debug("Adding native dependency: \(dependency.name), deps: \(nativeTarget.targetDependencies.map { $0.0 })")
targetDependencies.append(contentsOf: nativeTarget.targetDependencies.map { $0.1 })
nativeTarget.targetDependencies.forEach { target.add(dependency: $0.1) }
case .package:
// Packages don't have a transitive dependency field like native targets do, so we can't find dependency of a dependency from the project file
logger.debug("Adding package dependency: \(dependency.name)")
target.add(dependency: dependency)
case .externalProjectFramework:
// Can't move IR dependencies for prebuilt frameworks
continue
}
}

Expand Down Expand Up @@ -165,3 +195,37 @@ public struct XcodeProject {
return path
}
}

private func determineBuildPhaseFrameworkDependencies(_ target: PBXNativeTarget, with model: PBXProj) -> [PBXBuildFile] {
// Find the 'Link Binary with Libraries' build phase
let buildPhase = target.buildPhases
.compactMap { model.object(forKey: $0, as: PBXFrameworksBuildPhase.self) }
.first

guard let buildPhase else {
logger.debug("No PBXFrameworkBuild phase for target: \(target) found, continuing.")
return []
}

return buildPhase
.files
.compactMap { model.object(forKey: $0, as: PBXBuildFile.self) }
}

private func determineEmbeddedFrameworksDependencies(_ target: PBXNativeTarget, with model: PBXProj) -> [PBXBuildFile] {
// Find the "Embed Frameworks" build phase (copy files build phase)
let buildPhases = target
.buildPhases
.compactMap { model.object(forKey: $0, as: PBXCopyFilesBuildPhase.self) }

return buildPhases
.flatMap { $0.files }
.compactMap { model.object(forKey: $0, as: PBXBuildFile.self) }
.filter { file in
guard let ref = file.fileRef else { return false }
guard let object = model.object(forKey: ref, as: PBXFileReference.self) else { return false }
guard object.explicitFileType == "wrapper.framework" else { return false }

return true
}
}
26 changes: 19 additions & 7 deletions Sources/GenIR/BuildCacheManipulator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct BuildCacheManipulator {
/// Build settings used as part of the build
private let buildSettings: [String: String]

private let dryRun: Bool

/// Should we run the SKIP_INSTALL hack?
private let shouldDeploySkipInstallHack: Bool

Expand All @@ -19,14 +21,17 @@ struct BuildCacheManipulator {
case tooManyDirectories(String)
}

init(buildCachePath: URL, buildSettings: [String: String], archive: URL) throws {
init(buildCachePath: URL, buildSettings: [String: String], archive: URL, dryRun: Bool) throws {
self.buildCachePath = buildCachePath
self.buildSettings = buildSettings
self.dryRun = dryRun
buildProductsPath = archive
shouldDeploySkipInstallHack = self.buildSettings["SKIP_INSTALL"] == "NO"

guard FileManager.default.directoryExists(at: buildCachePath) else {
throw Error.directoryNotFound("Build cache path doesn't exist at expected path: \(buildCachePath)")
if !self.dryRun {
guard FileManager.default.directoryExists(at: buildCachePath) else {
throw Error.directoryNotFound("Build cache path doesn't exist at expected path: \(buildCachePath)")
}
}
}

Expand All @@ -41,10 +46,13 @@ struct BuildCacheManipulator {
do {
intermediateFolders = try FileManager.default.directories(at: intermediatesPath, recursive: false)
} catch {
throw Error.directoryNotFound("No directories found at \(intermediatesPath), expected exactly one. Ensure you did an archive build.")
throw Error.directoryNotFound(
"No directories found at \(intermediatesPath), expected exactly one. Ensure you did an archive build."
)
}

// TODO: Can we determine the main target being built here (via scheme or something similar?). That way we don't require a cleaned derived data
// TODO: Can we determine the main target being built here (via scheme or something similar?).
// That way we don't require a cleaned derived data
guard intermediateFolders.count == 1 else {
throw Error.tooManyDirectories(
"""
Expand All @@ -58,8 +66,12 @@ struct BuildCacheManipulator {
.appendingPathComponent(intermediateFolders.first!.lastPathComponent)
.appendingPathComponent("BuildProductsPath")

guard let archivePath = Self.findConfigurationDirectory(intermediatesBuildPath) else {
throw Error.directoryNotFound("Couldn't find or determine a build configuration directory (expected inside of: \(intermediatesBuildPath))")
guard
let archivePath = Self.findConfigurationDirectory(intermediatesBuildPath)
else {
throw Error.directoryNotFound(
"Couldn't find or determine a build configuration directory (expected inside of: \(intermediatesBuildPath))"
)
}

try skipInstallHack(archivePath)
Expand Down
Loading

0 comments on commit 1171006

Please sign in to comment.