Skip to content

Pass the "available modules" for a target down to Swift commands #7581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ public final class SwiftTargetBuildDescription {
/// Any macro products that this target requires to build.
public let requiredMacroProducts: [ResolvedProduct]

/// Path to a generated source file containing a listing of all of the
/// modules that are available to this target when it builds,
/// based on other targets in its package manifest and the products of
/// every package on which its package depends.
private var availableModules: AbsolutePath?

/// ObservabilityScope with which to emit diagnostics
private let observabilityScope: ObservabilityScope

Expand All @@ -260,6 +266,7 @@ public final class SwiftTargetBuildDescription {
testTargetRole: TestTargetRole? = nil,
shouldGenerateTestObservation: Bool = false,
shouldDisableSandbox: Bool,
availableModules: AvailableModules?,
fileSystem: FileSystem,
observabilityScope: ObservabilityScope
) throws {
Expand Down Expand Up @@ -319,6 +326,7 @@ public final class SwiftTargetBuildDescription {

try self.generateResourceEmbeddingCode()
try self.generateTestObservation()
try self.generateAvailableModules(availableModules)
}

private func generateTestObservation() throws {
Expand Down Expand Up @@ -348,6 +356,22 @@ public final class SwiftTargetBuildDescription {
try self.fileSystem.writeIfChanged(path: path, string: content)
}

private func generateAvailableModules(_ availableModules: AvailableModules?) throws {
guard defaultBuildParameters.driverParameters.isPackageAvailableModulesSupported else {
return
}

guard let availableModules else {
return
}

let path = self.tempsPath.appending(component: "available_modules.json")
let encoder = JSONEncoder.makeWithDefaults()
let data = try encoder.encode(availableModules)
try fileSystem.writeIfChanged(path: path, data: data)
self.availableModules = path
}

// FIXME: This will not work well for large files, as we will store the entire contents, plus its byte array
// representation in memory and also `writeIfChanged()` will read the entire generated file again.
private func generateResourceEmbeddingCode() throws {
Expand Down Expand Up @@ -659,6 +683,16 @@ public final class SwiftTargetBuildDescription {
isPackageNameSupported: self.defaultBuildParameters.driverParameters.isPackageAccessModifierSupported
)
)

if let availableModules {
result.append("-package-manifest")
result.append(self.package.underlying.path.pathString)
result.append("-package-available-modules")
result.append(availableModules.pathString)
result.append("-package-target-name")
result.append(self.target.name)
}

if !scanInvocation {
result.append("-emit-dependencies")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Basics
import Dispatch
import Foundation
import LLBuildManifest
import PackageGraph
import PackageModel
import SPMBuildCore
import SPMLLBuild
Expand Down
4 changes: 4 additions & 0 deletions Sources/Build/BuildPlan/BuildPlan+Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ extension BuildPlan {
toolsBuildParameters: toolsBuildParameters,
testTargetRole: .discovery,
shouldDisableSandbox: shouldDisableSandbox,
availableModules: nil,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
Expand Down Expand Up @@ -160,6 +161,7 @@ extension BuildPlan {
toolsBuildParameters: toolsBuildParameters,
testTargetRole: .entryPoint(isSynthesized: true),
shouldDisableSandbox: shouldDisableSandbox,
availableModules: nil,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
Expand Down Expand Up @@ -206,6 +208,7 @@ extension BuildPlan {
toolsBuildParameters: toolsBuildParameters,
testTargetRole: .entryPoint(isSynthesized: false),
shouldDisableSandbox: shouldDisableSandbox,
availableModules: nil,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
Expand All @@ -229,6 +232,7 @@ extension BuildPlan {
toolsBuildParameters: toolsBuildParameters,
testTargetRole: .entryPoint(isSynthesized: false),
shouldDisableSandbox: shouldDisableSandbox,
availableModules: nil,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
Expand Down
25 changes: 24 additions & 1 deletion Sources/Build/BuildPlan/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import OrderedCollections
import PackageGraph
import PackageLoading
import PackageModel
import SPMBuildCore
@_spi(SwiftPMInternal) import SPMBuildCore

#if USE_IMPL_ONLY_IMPORTS
@_implementationOnly import SwiftDriver
Expand Down Expand Up @@ -372,6 +372,20 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
// This can affect what flags to pass and other semantics.
let toolsVersion = graph.package(for: target)?.manifest.toolsVersion ?? .v5_5

// Retrieve the available modules for a given package.
var availableModulesCache: [PackageIdentity: AvailableModules] = [:]
func getAvailableModules(
package: ResolvedPackage
) -> AvailableModules? {
if let known = availableModulesCache[package.identity] {
return known
}

let availableModules = graph.availableModules(in: package)
availableModulesCache[package.identity] = availableModules
return availableModules
}

switch target.underlying {
case is SwiftTarget:
guard let package = graph.package(for: target) else {
Expand All @@ -388,6 +402,14 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
shouldGenerateTestObservation = false // Only generate the code once.
}

// Determine the set of available modules for this target.
let availableModules: AvailableModules?
if buildParameters.driverParameters.isPackageAccessModifierSupported {
availableModules = getAvailableModules(package: package)
} else {
availableModules = nil
}

targetMap[target.id] = try .swift(
SwiftTargetBuildDescription(
package: package,
Expand All @@ -401,6 +423,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
requiredMacroProducts: requiredMacroProducts,
shouldGenerateTestObservation: generateTestObservation,
shouldDisableSandbox: self.shouldDisableSandbox,
availableModules: availableModules,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
Expand Down
4 changes: 4 additions & 0 deletions Sources/DriverSupport/DriverSupportUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,8 @@ public enum DriverSupport {
public static func isPackageNameSupported(toolchain: PackageModel.Toolchain, fileSystem: FileSystem) -> Bool {
DriverSupport.checkToolchainDriverFlags(flags: ["-package-name"], toolchain: toolchain, fileSystem: fileSystem)
}

package static func isPackageAvailableModulesSupported(toolchain: PackageModel.Toolchain, fileSystem: FileSystem) -> Bool {
DriverSupport.checkToolchainDriverFlags(flags: ["-package-available-modules"], toolchain: toolchain, fileSystem: fileSystem)
}
}
133 changes: 133 additions & 0 deletions Sources/PackageGraph/AvailableModules.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2015-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Basics

/// Describes the set of modules that are available to targets within a
/// given package along with the target dependency that's required to import
/// that module into a given
package struct AvailableModules: Codable {
/// A description of the target dependency that would be needed to
/// import a given module.
package enum TargetDependency: Codable {
case target(name: String)
case product(name: String, package: String?)

static func <(lhs: TargetDependency, rhs: TargetDependency) -> Bool {
switch (lhs, rhs) {
case (.target(name: let lhsName), .target(name: let rhsName)):
lhsName < rhsName
case (.product(name: let lhsName, package: nil),
.product(name: let rhsName, package: nil)):
lhsName < rhsName
case (.product(name: _, package: nil),
.product(name: _, package: _?)):
true
case (.product(name: _, package: _?),
.product(name: _, package: nil)):
false
case (.product(name: let lhsName, package: let lhsPackage?),
.product(name: let rhsName, package: let rhsPackage?)):
(lhsPackage, lhsName) < (rhsPackage, rhsName)
case (.product, .target):
false
case (.target, .product):
true
}
}
}

/// The set of modules that are available within the package described by
/// the manifest, along with the target dependency required to reference
/// the module.
package var modules: [String: TargetDependency] = [:]
}

extension ModulesGraph {
/// A flat list of available modules, used as an intermediary for the
/// creation of an `AvailableModules` instance.
fileprivate typealias AvailableModulesList =
[(String, AvailableModules.TargetDependency)]

/// Collect the module names that are made available by all of the products
/// in this package, along with how the target dependency should be
/// expressed to make the corresponding modules importable.
///
/// The resulting module names are available to any package with a
/// dependency on this package.
fileprivate func productsAsAvailableModules(
from package: ResolvedPackage
) -> AvailableModulesList {
package.products.flatMap { product in
let productDependency: AvailableModules.TargetDependency = .product(
name: product.name,
package: product.packageIdentity.description
)

return product.targets.map { target in
(target.c99name, productDependency)
}
}
}

/// Collect the module names that are made available by all of the targets
/// in this package, along with how the target dependency should be
/// expressed to make the corresponding module importable.
///
/// The resulting module names are available within the targets of this
/// package.
fileprivate func targetsAsAvailableModules(
in package: ResolvedPackage
) -> AvailableModulesList {
package.targets.map { target in
(target.c99name, .target(name: target.name))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is guaranteed to be correct for C targets or binary targets, and it should exclude any plugin and macro targets

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For binary targets we at least tell people to match the target name and module name, but we aren't verifying it.

}
}

/// Produce the complete set of modules that are available within the
/// given resolved package.
package func availableModules(
in package: ResolvedPackage
) -> AvailableModules {
var availableModules = AvailableModules()

// Add available modules from targets within this package.
availableModules.modules.merge(
targetsAsAvailableModules(in: package),
uniquingKeysWith: uniqueTargetDependency
)

// Add available modules from the products of any package this package
// depends on.
for dependencyID in package.dependencies {
guard let dependencyPackage = self.package(for: dependencyID) else {
continue
}

availableModules.modules.merge(
productsAsAvailableModules(from: dependencyPackage),
uniquingKeysWith: uniqueTargetDependency
)
}

return availableModules
}
}

/// "Unique" two target dependencies by picking the target dependency that we
/// prefer.
fileprivate func uniqueTargetDependency(
lhs: AvailableModules.TargetDependency,
rhs: AvailableModules.TargetDependency
) -> AvailableModules.TargetDependency {
lhs < rhs ? lhs : rhs
}
1 change: 1 addition & 0 deletions Sources/PackageGraph/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_library(PackageGraph
AvailableModules.swift
BoundVersion.swift
BuildTriple.swift
DependencyMirrors.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ extension BuildParameters {
explicitTargetDependencyImportCheckingMode: TargetDependencyImportCheckingMode = .none,
useIntegratedSwiftDriver: Bool = false,
useExplicitModuleBuild: Bool = false,
isPackageAccessModifierSupported: Bool = false
isPackageAccessModifierSupported: Bool = false,
isPackageAvailableModulesSupported: Bool = false
) {
self.canRenameEntrypointFunctionName = canRenameEntrypointFunctionName
self.enableParseableModuleInterfaces = enableParseableModuleInterfaces
self.explicitTargetDependencyImportCheckingMode = explicitTargetDependencyImportCheckingMode
self.useIntegratedSwiftDriver = useIntegratedSwiftDriver
self.useExplicitModuleBuild = useExplicitModuleBuild
self.isPackageAccessModifierSupported = isPackageAccessModifierSupported
self.isPackageAvailableModulesSupported = isPackageAvailableModulesSupported
}

/// Whether to enable the entry-point-function-name feature.
Expand All @@ -58,5 +60,10 @@ extension BuildParameters {
/// supports `-package-name` options.
@_spi(SwiftPMInternal)
public var isPackageAccessModifierSupported: Bool

/// Whether the version of Swift Driver used in the currently selected
/// toolchain supports '-package-available-modules' options.
@_spi(SwiftPMInternal)
public var isPackageAvailableModulesSupported: Bool
}
}
4 changes: 4 additions & 0 deletions Sources/swift-bootstrap/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ struct SwiftBootstrapBuildTool: ParsableCommand {
isPackageAccessModifierSupported: DriverSupport.isPackageNameSupported(
toolchain: targetToolchain,
fileSystem: self.fileSystem
),
isPackageAvailableModulesSupported: DriverSupport.isPackageAvailableModulesSupported(
toolchain: targetToolchain,
fileSystem: self.fileSystem
)
),
linkingParameters: .init(
Expand Down
17 changes: 17 additions & 0 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,23 @@ final class BuildPlanTests: XCTestCase {
}
}

func testPackageAvailableModulesFlag() throws {
let isFlagSupportedInDriver = try DriverSupport.checkToolchainDriverFlags(
flags: ["package-available-modules"],
toolchain: UserToolchain.default,
fileSystem: localFileSystem
)

try fixture(name: "Miscellaneous/PackageNameFlag") { fixturePath in
let (stdout, _) = try executeSwiftBuild(fixturePath.appending("appPkg"), extraArgs: ["-vv"])

if isFlagSupportedInDriver {
XCTAssertMatch(stdout, .contains("-package-target-name App"))
XCTAssertMatch(stdout, .contains("-package-available-modules"))
}
}
}

#if os(macOS)
func testPackageNameFlagXCBuild() throws {
let isFlagSupportedInDriver = try DriverSupport.checkToolchainDriverFlags(
Expand Down