diff --git a/Fixtures/PartiallyUnusedDependency/Dep/Package.swift b/Fixtures/PartiallyUnusedDependency/Dep/Package.swift new file mode 100644 index 00000000000..4046c584594 --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Dep/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Dep", + products: [ + .library( + name: "MyDynamicLibrary", + type: .dynamic, + targets: ["MyDynamicLibrary"] + ), + .executable( + name: "MySupportExecutable", + targets: ["MySupportExecutable"] + ) + ], + targets: [ + .target( + name: "MyDynamicLibrary" + ), + .executableTarget( + name: "MySupportExecutable", + dependencies: ["MyDynamicLibrary"] + ) + ] +) diff --git a/Fixtures/PartiallyUnusedDependency/Dep/Sources/MyDynamicLibrary/Dep.swift b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MyDynamicLibrary/Dep.swift new file mode 100644 index 00000000000..760f5831aad --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MyDynamicLibrary/Dep.swift @@ -0,0 +1,3 @@ +public func sayHello() { + print("hello!") +} diff --git a/Fixtures/PartiallyUnusedDependency/Dep/Sources/MySupportExecutable/exe.swift b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MySupportExecutable/exe.swift new file mode 100644 index 00000000000..063d151101c --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MySupportExecutable/exe.swift @@ -0,0 +1,8 @@ +import MyDynamicLibrary + +@main struct Entry { + static func main() { + print("running support tool") + sayHello() + } +} diff --git a/Fixtures/PartiallyUnusedDependency/Package.swift b/Fixtures/PartiallyUnusedDependency/Package.swift new file mode 100644 index 00000000000..40d5345f5db --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "PartiallyUnusedDependency", + products: [ + .executable( + name: "MyExecutable", + targets: ["MyExecutable"] + ), + ], + dependencies: [ + .package(path: "Dep") + ], + targets: [ + .executableTarget( + name: "MyExecutable", + dependencies: [.product(name: "MyDynamicLibrary", package: "Dep")] + ), + .plugin( + name: "dump-artifacts-plugin", + capability: .command( + intent: .custom(verb: "dump-artifacts-plugin", description: "Dump Artifacts"), + permissions: [] + ) + ) + ] +) diff --git a/Fixtures/PartiallyUnusedDependency/Plugins/dump-artifacts-plugin.swift b/Fixtures/PartiallyUnusedDependency/Plugins/dump-artifacts-plugin.swift new file mode 100644 index 00000000000..cebbb779ee3 --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Plugins/dump-artifacts-plugin.swift @@ -0,0 +1,24 @@ +import PackagePlugin + +@main +struct DumpArtifactsPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + do { + var parameters = PackageManager.BuildParameters() + parameters.configuration = .debug + parameters.logging = .concise + let result = try packageManager.build(.all(includingTests: false), parameters: parameters) + print("succeeded: \(result.succeeded)") + for artifact in result.builtArtifacts { + print("artifact-path: \(artifact.path.string)") + print("artifact-kind: \(artifact.kind)") + } + } + catch { + print("error from the plugin host: \\(error)") + } + } +} diff --git a/Fixtures/PartiallyUnusedDependency/Sources/MyExecutable/PartiallyUnusedDependency.swift b/Fixtures/PartiallyUnusedDependency/Sources/MyExecutable/PartiallyUnusedDependency.swift new file mode 100644 index 00000000000..daa68135bb4 --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Sources/MyExecutable/PartiallyUnusedDependency.swift @@ -0,0 +1,8 @@ +import MyDynamicLibrary + +@main struct Entry { + static func main() { + print("Hello, world!") + sayHello() + } +} diff --git a/Sources/Build/BuildOperation.swift b/Sources/Build/BuildOperation.swift index f162c921030..c720b6c0e67 100644 --- a/Sources/Build/BuildOperation.swift +++ b/Sources/Build/BuildOperation.swift @@ -461,11 +461,37 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS let buildResultBuildPlan = buildOutputs.contains(.buildPlan) ? try buildPlan : nil let buildResultReplArgs = buildOutputs.contains(.replArguments) ? try buildPlan.createREPLArguments() : nil + let artifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? + if buildOutputs.contains(.builtArtifacts) { + let builtProducts = try buildPlan.buildProducts + artifacts = try builtProducts.compactMap { + switch $0.product.type { + case .library(let kind): + let artifactKind: PluginInvocationBuildResult.BuiltArtifact.Kind + switch kind { + case .dynamic: artifactKind = .dynamicLibrary + case .static, .automatic: artifactKind = .staticLibrary + } + return try ($0.product.name, .init( + path: $0.binaryPath.pathString, + kind: artifactKind) + ) + case .executable: + return try ($0.product.name, .init(path: $0.binaryPath.pathString, kind: .executable)) + default: + return nil + } + } + } else { + artifacts = nil + } + result = BuildResult( serializedDiagnosticPathsByTargetName: result.serializedDiagnosticPathsByTargetName, symbolGraph: result.symbolGraph, buildPlan: buildResultBuildPlan, replArguments: buildResultReplArgs, + builtArtifacts: artifacts ) var serializedDiagnosticPaths: [String: [AbsolutePath]] = [:] do { diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index d3dbb38c22a..715da6aad51 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -178,40 +178,21 @@ final class PluginDelegate: PluginInvocationDelegate { ) // Run the build. This doesn't return until the build is complete. - let success = await buildSystem.buildIgnoringError(subset: buildSubset) + let result = await buildSystem.buildIgnoringError(subset: buildSubset, buildOutputs: [.builtArtifacts]) + let success = result != nil let packageGraph = try await buildSystem.getPackageGraph() - var builtArtifacts: [PluginInvocationBuildResult.BuiltArtifact] = [] - - for rootPkg in packageGraph.rootPackages { - let builtProducts = rootPkg.products.filter { - switch subset { - case .all(let includingTests): - return includingTests ? true : $0.type != .test - case .product(let name): - return $0.name == name - case .target(let name): - return $0.name == name - } - } - - let artifacts: [PluginInvocationBuildResult.BuiltArtifact] = try builtProducts.compactMap { - switch $0.type { - case .library(let kind): - return .init( - path: try buildParameters.binaryPath(for: $0).pathString, - kind: (kind == .dynamic) ? .dynamicLibrary : .staticLibrary - ) - case .executable: - return .init(path: try buildParameters.binaryPath(for: $0).pathString, kind: .executable) - default: - return nil - } + var builtArtifacts: [PluginInvocationBuildResult.BuiltArtifact] = (result?.builtArtifacts ?? []).filter { (name, _) in + switch subset { + case .all(let includingTests): + return true + case .product(let productName): + return name == productName + case .target(let targetName): + return name == targetName } - - builtArtifacts.append(contentsOf: artifacts) - } + }.map(\.1) return PluginInvocationBuildResult( succeeded: success, @@ -495,12 +476,11 @@ final class PluginDelegate: PluginInvocationDelegate { } extension BuildSystem { - fileprivate func buildIgnoringError(subset: BuildSubset) async -> Bool { + fileprivate func buildIgnoringError(subset: BuildSubset, buildOutputs: [BuildOutput]) async -> BuildResult? { do { - try await self.build(subset: subset, buildOutputs: []) - return true + return try await self.build(subset: subset, buildOutputs: buildOutputs) } catch { - return false + return nil } } } @@ -533,4 +513,4 @@ fileprivate extension BuildOutput.SymbolGraphAccessLevel { .open } } -} \ No newline at end of file +} diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index d5261236271..701e1952475 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -39,32 +39,6 @@ public enum BuildSubset { /// build systems can produce all possible build outputs. Check the build /// result for indication that the output was produced. public enum BuildOutput: Equatable { - public static func == (lhs: BuildOutput, rhs: BuildOutput) -> Bool { - switch lhs { - case .symbolGraph(let leftOptions): - switch rhs { - case .symbolGraph(let rightOptions): - return leftOptions == rightOptions - default: - return false - } - case .buildPlan: - switch rhs { - case .buildPlan: - return true - default: - return false - } - case .replArguments: - switch rhs { - case .replArguments: - return true - default: - return false - } - } - } - public enum SymbolGraphAccessLevel: String { case `private`, `fileprivate`, `internal`, `package`, `public`, `open` } @@ -93,6 +67,7 @@ public enum BuildOutput: Equatable { case symbolGraph(SymbolGraphOptions) case buildPlan case replArguments + case builtArtifacts } /// A protocol that represents a build system used by SwiftPM for all build operations. This allows factoring out the @@ -144,12 +119,14 @@ public struct BuildResult { serializedDiagnosticPathsByTargetName: Result<[String: [AbsolutePath]], Error>, symbolGraph: SymbolGraphResult? = nil, buildPlan: BuildPlan? = nil, - replArguments: CLIArguments? + replArguments: CLIArguments?, + builtArtifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? = nil ) { self.serializedDiagnosticPathsByTargetName = serializedDiagnosticPathsByTargetName self.symbolGraph = symbolGraph self.buildPlan = buildPlan self.replArguments = replArguments + self.builtArtifacts = builtArtifacts } public let replArguments: CLIArguments? @@ -157,6 +134,7 @@ public struct BuildResult { public let buildPlan: BuildPlan? public var serializedDiagnosticPathsByTargetName: Result<[String: [AbsolutePath]], Error> + public var builtArtifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? } public protocol ProductBuildDescription { diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 13713ef9819..535ac4e1838 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -351,30 +351,28 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { try await writePIF(buildParameters: buildParameters) - var symbolGraphOptions: BuildOutput.SymbolGraphOptions? - for output in buildOutputs { - switch output { - case .symbolGraph(let options): - symbolGraphOptions = options - default: - continue - } - } - return try await startSWBuildOperation( pifTargetName: subset.pifTargetName, - symbolGraphOptions: symbolGraphOptions, - generateReplArguments: buildOutputs.contains(.replArguments), + buildOutputs: buildOutputs, ) } private func startSWBuildOperation( pifTargetName: String, - symbolGraphOptions: BuildOutput.SymbolGraphOptions?, - generateReplArguments: Bool + buildOutputs: [BuildOutput] ) async throws -> BuildResult { let buildStartTime = ContinuousClock.Instant.now + var symbolGraphOptions: BuildOutput.SymbolGraphOptions? + for output in buildOutputs { + switch output { + case .symbolGraph(let options): + symbolGraphOptions = options + default: + continue + } + } var replArguments: CLIArguments? + var artifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? return try await withService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) { service in let derivedDataPath = self.buildParameters.dataPath @@ -555,11 +553,19 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { let operation = try await session.createBuildOperation( request: request, - delegate: PlanningOperationDelegate() + delegate: PlanningOperationDelegate(), + retainBuildDescription: true ) + var buildDescriptionID: SWBBuildDescriptionID? = nil var buildState = BuildState() for try await event in try await operation.start() { + if case .reportBuildDescription(let info) = event { + if buildDescriptionID != nil { + self.observabilityScope.emit(debug: "build unexpectedly reported multiple build description IDs") + } + buildDescriptionID = SWBBuildDescriptionID(info.buildDescriptionID) + } try emitEvent(event, buildState: &buildState) } @@ -585,7 +591,46 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { throw Diagnostics.fatalError } - replArguments = generateReplArguments ? try await self.createREPLArguments(session: session, request: request) : nil + if buildOutputs.contains(.replArguments) { + replArguments = try await self.createREPLArguments(session: session, request: request) + } + + if buildOutputs.contains(.builtArtifacts) { + if let buildDescriptionID { + let targetInfo = try await session.configuredTargets(buildDescription: buildDescriptionID, buildRequest: request) + artifacts = targetInfo.compactMap { target in + guard let artifactInfo = target.artifactInfo else { + return nil + } + let kind: PluginInvocationBuildResult.BuiltArtifact.Kind = switch artifactInfo.kind { + case .executable: + .executable + case .staticLibrary: + .staticLibrary + case .dynamicLibrary: + .dynamicLibrary + case .framework: + // We treat frameworks as dylibs here, but the plugin API should grow to accomodate more product types + .dynamicLibrary + } + var name = target.name + // FIXME: We need a better way to map between SwiftPM target/product names and PIF target names + if pifTargetName.hasSuffix("-product") { + name = String(name.dropLast(8)) + } + return (name, .init( + path: artifactInfo.path, + kind: kind + )) + } + } else { + self.observabilityScope.emit(error: "failed to compute built artifacts list") + } + } + + if let buildDescriptionID { + await session.releaseBuildDescription(id: buildDescriptionID) + } } } catch let sessError as SessionFailedError { for diagnostic in sessError.diagnostics { @@ -604,6 +649,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } ), replArguments: replArguments, + builtArtifacts: artifacts ) } } diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index de7e948da35..30ecf38a3dc 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -6285,33 +6285,38 @@ struct PackageCommandTests { #expect(stdout.contains("executable")) } - // Invoke the plugin with parameters choosing a verbose build of MyStaticLibrary for release. - do { - let (stdout, _) = try await execute( - ["my-build-tester", "--product", "MyStaticLibrary", "--print-commands", "--release"], - packagePath: packageDir, - configuration: data.config, - buildSystem: data.buildSystem, - ) - #expect(stdout.contains("Building for production...")) - #expect(!stdout.contains("Building for debug...")) - #expect(!stdout.contains("-module-name MyLibrary")) - if buildSystemProvider == .native { - #expect(stdout.contains("Build of product 'MyStaticLibrary' complete!")) - } - #expect(stdout.contains("succeeded: true")) - switch buildSystemProvider { - case .native: - #expect(stdout.contains("artifact-path:")) - #expect(stdout.contains(RelativePath("release/libMyStaticLibrary").pathString)) - case .swiftbuild: - #expect(stdout.contains("artifact-path:")) - #expect(stdout.contains(RelativePath("MyStaticLibrary").pathString)) - case .xcode: - Issue.record("unimplemented assertion for --build-system xcode") + // SwiftBuild is currently not producing a static archive for static products unless they are linked into some other binary. + try await withKnownIssue { + // Invoke the plugin with parameters choosing a verbose build of MyStaticLibrary for release. + do { + let (stdout, _) = try await execute( + ["my-build-tester", "--product", "MyStaticLibrary", "--print-commands", "--release"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Building for production...")) + #expect(!stdout.contains("Building for debug...")) + #expect(!stdout.contains("-module-name MyLibrary")) + if buildSystemProvider == .native { + #expect(stdout.contains("Build of product 'MyStaticLibrary' complete!")) + } + #expect(stdout.contains("succeeded: true")) + switch buildSystemProvider { + case .native: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("release/libMyStaticLibrary").pathString)) + case .swiftbuild: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("MyStaticLibrary").pathString)) + case .xcode: + Issue.record("unimplemented assertion for --build-system xcode") + } + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("staticLibrary")) } - #expect(stdout.contains("artifact-kind:")) - #expect(stdout.contains("staticLibrary")) + } when: { + data.buildSystem == .swiftbuild } // Invoke the plugin with parameters choosing a verbose build of MyDynamicLibrary for release. @@ -6355,6 +6360,36 @@ struct PackageCommandTests { } } + @Test( + .IssueWindowsRelativePathAssert, + arguments: [BuildSystemProvider.Kind.native, .swiftbuild], + ) + func commandPluginBuildingCallbacksExcludeUnbuiltArtifacts(buildSystem: BuildSystemProvider.Kind) async throws { + try await withKnownIssue { + try await fixture(name: "PartiallyUnusedDependency") { fixturePath in + let (stdout, _) = try await execute( + ["dump-artifacts-plugin"], + packagePath: fixturePath, + configuration: .debug, + buildSystem: buildSystem + ) + // The build should succeed + #expect(stdout.contains("succeeded: true")) + // The artifacts corresponding to the executable and dylib we built should be reported + #expect(stdout.contains(#/artifact-path: [^\n]+MyExecutable(.*)?\nartifact-kind: executable/#)) + #expect(stdout.contains(#/artifact-path: [^\n]+MyDynamicLibrary(.*)?\nartifact-kind: dynamicLibrary/#)) + // The not-built executable in the dependency should not be reported. The native build system fails to exclude it. + withKnownIssue { + #expect(!stdout.contains("MySupportExecutable")) + } when: { + buildSystem == .native + } + } + } when: { + buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows + } + } + @Test( .IssueWindowsRelativePathAssert, .requiresSwiftConcurrencySupport,