Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/SWBCore/ProjectModel/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ public final class Workspace: ProjectModelItem, PIFObject, ReferenceLookupContex
return project
}

public static func projectLocation(for target: Target?, workspace: Workspace?) -> Diagnostic.Location {
if let target, let workspace {
let path = workspace.project(for: target).xcodeprojPath
return Diagnostic.Location.path(path, fileLocation: nil)
}
return .unknown
}

/// Find the projects with the given name.
public func projects(named name: String) -> [Project] {
return projectsByName[name] ?? []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1463,7 +1463,7 @@ open class GenericOutputParser : TaskOutputParser {
public let toolBasenames: Set<String>

/// Buffered output that has not yet been parsed (parsing is line-by-line, so we buffer incomplete lines until we receive more output).
private var unparsedBytes: ArraySlice<UInt8> = []
internal var unparsedBytes: ArraySlice<UInt8> = []

/// The Diagnostic that is being constructed, possibly across multiple lines of input.
private var inProgressDiagnostic: Diagnostic?
Expand Down
69 changes: 65 additions & 4 deletions Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,60 @@ public struct DiscoveredLdLinkerToolSpecInfo: DiscoveredCommandLineToolSpecInfo
/// Maximum number of undefined symbols to emit. Might be configurable in the future.
let undefinedSymbolCountLimit = 100

override public func write(bytes: ByteString) {

// Split the buffer into slices separated by newlines. The last slice represents the partial last line (there always is one, even if it's empty).
var lines = bytes.split(separator: UInt8(ascii: "\n"), maxSplits: .max, omittingEmptySubsequences: false)

// Any unparsed bytes belong to the first line. We don't want to run `split` over these because it can lead to accidentally quadratic behavior if write is called many times per line.
lines[0] = unparsedBytes + lines[0]

let linesToParse = lines.dropLast()

if let target = self.task.forTarget?.target {
// Linker errors and warnings take more effort to get actionable information out of build logs than those for source files. This is because the linker does not have the path to the project or target name so they are not included in the message.
//
// Prepend the path to the project and target name to any error or warning lines.
// Example input:
// ld: warning: linking with (/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio) but not using any symbols from it
// Example output:
// /Path/To/ProjectFolder/ProjectName.xcodeproj: TargetName: ld: warning: linking with (/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio) but not using any symbols from it

let workspace = self.workspaceContext.workspace
let projectPath = workspace.project(for: target).xcodeprojPath
let targetName = target.name

let processedLines: [ByteString] = linesToParse.map { lineBytes in
let lineString = String(decoding: lineBytes, as: Unicode.UTF8.self)
if lineString.contains(": error:")
|| lineString.contains(": warning:") {

let issueString = "\(projectPath.str): \(targetName): \(lineString)"
return ByteString(encodingAsUTF8: issueString)
}
return ByteString(lineBytes)
}

// Forward the bytes
let processedBytes = ByteString(processedLines.joined(separator: ByteString("\n")))
delegate.emitOutput(processedBytes)
}
else {
// Forward the bytes
let processedBytes = ByteString(linesToParse.joined(separator: ByteString("\n")))
delegate.emitOutput(processedBytes)
}

// Parse any complete lines of output.
for line in linesToParse {
parseLine(line)
}

// Track the last, incomplete line to as the unparsed bytes.
unparsedBytes = lines.last ?? []
}

@discardableResult
override func parseLine<S: Collection>(_ lineBytes: S) -> Bool where S.Element == UInt8 {

// Create a string that we can examine. Use the non-failable constructor, so that we are robust against potentially invalid UTF-8.
Expand All @@ -190,11 +244,14 @@ public struct DiscoveredLdLinkerToolSpecInfo: DiscoveredCommandLineToolSpecInfo
}
else if let match = LdLinkerOutputParser.problemMessageRegEx.firstMatch(in: lineString), match[3].hasPrefix("symbol(s) not found") {
// It's time to emit all the symbols. We emit each as a separate message.
let projectLocation = Workspace.projectLocation(for: self.task.forTarget?.target,
workspace: self.workspaceContext.workspace)

for symbol in undefinedSymbols.prefix(undefinedSymbolCountLimit) {
delegate.diagnosticsEngine.emit(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData("Undefined symbol: \(symbol)"), appendToOutputStream: false))
delegate.diagnosticsEngine.emit(Diagnostic(behavior: .error, location: projectLocation, data: DiagnosticData("Undefined symbol: \(symbol)"), appendToOutputStream: false))
}
if undefinedSymbols.count > undefinedSymbolCountLimit {
delegate.diagnosticsEngine.emit(Diagnostic(behavior: .note, location: .unknown, data: DiagnosticData("(\(undefinedSymbols.count - undefinedSymbolCountLimit) additional undefined symbols are shown in the transcript"), appendToOutputStream: false))
delegate.diagnosticsEngine.emit(Diagnostic(behavior: .note, location: projectLocation, data: DiagnosticData("(\(undefinedSymbols.count - undefinedSymbolCountLimit) additional undefined symbols are shown in the transcript"), appendToOutputStream: false))
}
collectingUndefinedSymbols = false
undefinedSymbols = []
Expand All @@ -213,7 +270,9 @@ public struct DiscoveredLdLinkerToolSpecInfo: DiscoveredCommandLineToolSpecInfo
let severity = match[2].isEmpty ? "error" : match[2]
let behavior = Diagnostic.Behavior(name: severity) ?? .note
let message = match[3].prefix(1).localizedCapitalized + match[3].dropFirst()
let diagnostic = Diagnostic(behavior: behavior, location: .unknown, data: DiagnosticData(message), appendToOutputStream: false)
let projectLocation = Workspace.projectLocation(for: self.task.forTarget?.target,
workspace: self.workspaceContext.workspace)
let diagnostic = Diagnostic(behavior: behavior, location: projectLocation, data: DiagnosticData(message), appendToOutputStream: false)
delegate.diagnosticsEngine.emit(diagnostic)
}
return true
Expand Down Expand Up @@ -729,7 +788,9 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
enumerateLinkerCommandLine(arguments: commandLine, handleWl: cbc.scope.evaluate(BuiltinMacros._DISCOVER_COMMAND_LINE_LINKER_INPUTS_INCLUDE_WL)) { arg, value in
func emitDependencyDiagnostic(type: String, node: PlannedPathNode) {
if delegate.userPreferences.enableDebugActivityLogs {
delegate.note("Added \(type) dependency '\(node.path.str)' from command line argument \(arg)", location: .unknown)
let projectLocation = cbc.producer.projectLocation

delegate.note("Added \(type) dependency '\(node.path.str)' from command line argument \(arg)", location: projectLocation)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/SWBCore/TaskGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
/// - Returns: Information on the headers referenced by the project that the given target is a part of.
func projectHeaderInfo(for target: Target) async -> ProjectHeaderInfo?

var projectLocation: Diagnostic.Location { get }

/// Whether or not an SDK stat cache should be used for the build of this target.
func shouldUseSDKStatCache() async -> Bool

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,10 @@ extension TaskProducerContext: CommandProducer {
return await workspaceContext.headerIndex.projectHeaderInfo[workspaceContext.workspace.project(for: target)]
}

public var projectLocation: Diagnostic.Location {
return Workspace.projectLocation(for: self.configuredTarget?.target, workspace: self.workspaceContext.workspace)
}

public var canConstructAppIntentsMetadataTask: Bool {
let scope = settings.globalScope
let buildComponents = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS)
Expand Down
4 changes: 4 additions & 0 deletions Sources/SWBTestSupport/DummyCommandProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ package struct MockCommandProducer: CommandProducer, Sendable {
return nil
}

public var projectLocation: Diagnostic.Location {
return .unknown
}

package func discoveredCommandLineToolSpecInfo(_ delegate: any SWBCore.CoreClientTargetDiagnosticProducingDelegate, _ toolName: String, _ path: Path, _ process: @Sendable (Data) async throws -> any SWBCore.DiscoveredCommandLineToolSpecInfo) async throws -> any SWBCore.DiscoveredCommandLineToolSpecInfo {
try await discoveredCommandLineToolSpecInfoCache.run(delegate, toolName, path, process)
}
Expand Down
5 changes: 3 additions & 2 deletions Tests/SWBBuildSystemTests/LinkerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ fileprivate struct LinkerTests: CoreBasedTests {
func linkerDriverDiagnosticsParsing() async throws {

try await withTemporaryDirectory { tmpDir in
let projectName = "TestProject"
let testProject = try await TestProject(
"TestProject",
projectName,
sourceRoot: tmpDir,
groupTree: TestGroup(
"SomeFiles",
Expand Down Expand Up @@ -61,7 +62,7 @@ fileprivate struct LinkerTests: CoreBasedTests {
}

try await tester.checkBuild(runDestination: .macOS) { results in
results.checkError(.prefix("Unknown argument: '-not-a-real-flag'"))
results.checkError(.prefix("\(tmpDir.str)/\(projectName).xcodeproj: Unknown argument: '-not-a-real-flag'"))
results.checkError(.prefix("Command Ld failed."))
}
}
Expand Down