diff --git a/Sources/SWBCore/ProjectModel/Workspace.swift b/Sources/SWBCore/ProjectModel/Workspace.swift index d1c8af8b..f2c68c93 100644 --- a/Sources/SWBCore/ProjectModel/Workspace.swift +++ b/Sources/SWBCore/ProjectModel/Workspace.swift @@ -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] ?? [] diff --git a/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift b/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift index 5b5016c4..8bf3bb03 100644 --- a/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift +++ b/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift @@ -1463,7 +1463,7 @@ open class GenericOutputParser : TaskOutputParser { public let toolBasenames: Set /// 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 = [] + internal var unparsedBytes: ArraySlice = [] /// The Diagnostic that is being constructed, possibly across multiple lines of input. private var inProgressDiagnostic: Diagnostic? diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift index 69a87c72..25b74372 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift @@ -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(_ 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. @@ -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 = [] @@ -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 @@ -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) } } diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index 53db7891..aa9e5712 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -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 diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index d5397dac..add8c260 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -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) diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift index 1b0e9e9a..32fd50f4 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -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) } diff --git a/Tests/SWBBuildSystemTests/LinkerTests.swift b/Tests/SWBBuildSystemTests/LinkerTests.swift index 97f61702..8991b51e 100644 --- a/Tests/SWBBuildSystemTests/LinkerTests.swift +++ b/Tests/SWBBuildSystemTests/LinkerTests.swift @@ -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", @@ -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.")) } }