Skip to content

Commit

Permalink
Merge pull request #209 from polac24/add-driver-parsing
Browse files Browse the repository at this point in the history
Swift-driver integration, Part II: add Swift front-end parsing stage
  • Loading branch information
polac24 authored Jun 1, 2023
2 parents ee31a38 + 867bbb6 commit 65bf915
Show file tree
Hide file tree
Showing 15 changed files with 1,009 additions and 12 deletions.
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ let package = Package(
name: "xcswiftc",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcswift-frontend",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtoolSupport",
dependencies: ["XCRemoteCache"]
Expand Down Expand Up @@ -69,6 +73,7 @@ let package = Package(
dependencies: [
"xcprebuild",
"xcswiftc",
"xcswift-frontend",
"xclibtool",
"xcpostbuild",
"xcprepare",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ Note: This step is not required if at least one of these is true:
| `custom_rewrite_envs` | A list of extra ENVs that should be used as placeholders in the dependency list. ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process. | `[]` | ⬜️ |
| `irrelevant_dependencies_paths` | Regexes of files that should not be included in a list of dependencies. Warning! Add entries here with caution - excluding dependencies that are relevant might lead to a target overcaching. The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude all `.modulemap` files. | `[]` | ⬜️ |
| `gracefully_handle_missing_common_sha` | If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch. That might be useful on CI, where a shallow clone is used and cloning depth is not big enough to fetch a commit from a primary branch | `false` | ⬜️ |
| `enable_swift_driver_integration` | Enable experimental integration with swift driver, added in Xcode 14 | `false` | ⬜️ |

## Backend cache server

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,44 @@ struct IntegrateContext {
let configOverride: URL
let fakeSrcRoot: URL
let output: URL?
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption
}

extension IntegrateContext {
init(
input: String,
repoRootPath: String,
config: XCRemoteCacheConfig,
mode: Mode,
configOverridePath: String,
env: [String: String],
binariesDir: URL,
fakeSrcRoot: String,
outputPath: String?
) throws {
projectPath = URL(fileURLWithPath: input)
let srcRoot = projectPath.deletingLastPathComponent()
repoRoot = URL(fileURLWithPath: repoRootPath, relativeTo: srcRoot)
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: srcRoot)
self.mode = mode
configOverride = URL(fileURLWithPath: configOverridePath, relativeTo: srcRoot)
configOverride = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: srcRoot)
output = outputPath.flatMap(URL.init(fileURLWithPath:))
self.fakeSrcRoot = URL(fileURLWithPath: fakeSrcRoot)
var swiftcBinaryName = "swiftc"
var buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = []
// Keep the legacy behaviour (supported in Xcode 14 and lower)
if !config.enableSwiftDriverIntegration {
buildSettingsAppenderOptions.insert(.disableSwiftDriverIntegration)
swiftcBinaryName = "xcswiftc"
}
binaries = XCRCBinariesPaths(
prepare: binariesDir.appendingPathComponent("xcprepare"),
cc: binariesDir.appendingPathComponent("xccc"),
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
swiftc: binariesDir.appendingPathComponent(swiftcBinaryName),
libtool: binariesDir.appendingPathComponent("xclibtool"),
lipo: binariesDir.appendingPathComponent("xclipo"),
ld: binariesDir.appendingPathComponent("xcld"),
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
)
self.buildSettingsAppenderOptions = buildSettingsAppenderOptions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,8 @@ public class XCIntegrate {

let context = try IntegrateContext(
input: projectPath,
repoRootPath: config.repoRoot,
config: config,
mode: mode,
configOverridePath: config.extraConfigurationFile,
env: env,
binariesDir: binariesDir,
fakeSrcRoot: fakeSrcRoot,
Expand All @@ -98,15 +97,12 @@ public class XCIntegrate {
excludes: targetsExclude.integrateArrayArguments,
includes: targetsInclude.integrateArrayArguments
)
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = [
.disableSwiftDriverIntegration,
]
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
mode: context.mode,
repoRoot: context.repoRoot,
fakeSrcRoot: context.fakeSrcRoot,
sdksExclude: sdksExclude.integrateArrayArguments,
options: buildSettingsAppenderOptions
options: context.buildSettingsAppenderOptions
)
let lldbPatcher: LLDBInitPatcher
switch lldbMode {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import Foundation

enum SwiftFrontendArgInputError: Error, Equatable {
// swift-frontend should either be compling or emiting a module
case bothCompilationAndEmitAction
// no .swift files have been passed as input files
case noCompilationInputs
// no -primary-file .swift files have been passed as input files
case noPrimaryFileCompilationInputs
// number of -emit-dependencies-path doesn't match compilation inputs
case dependenciesOuputCountDoesntMatch(expected: Int, parsed: Int)
// number of -serialize-diagnostics-path doesn't match compilation inputs
case diagnosticsOuputCountDoesntMatch(expected: Int, parsed: Int)
// number of -o doesn't match compilation inputs
case outputsOuputCountDoesntMatch(expected: Int, parsed: Int)
// number of -o for emit-module can be only 1
case emitModulOuputCountIsNot1(parsed: Int)
// number of -emit-dependencies-path for emit-module can be 0 or 1 (generate or not)
case emitModuleDependenciesOuputCountIsHigherThan1(parsed: Int)
// number of -serialize-diagnostics-path for emit-module can be 0 or 1 (generate or not)
case emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: Int)
// emit-module requires -emit-objc-header-path
case emitModuleMissingObjcHeaderPath
// -target is required
case emitMissingTarget
// -moduleName is required
case emitMissingModuleName
}

public struct SwiftFrontendArgInput {
let compile: Bool
let emitModule: Bool
let objcHeaderOutput: String?
let moduleName: String?
let target: String?
let primaryInputPaths: [String]
let inputPaths: [String]
var outputPaths: [String]
var dependenciesPaths: [String]
// Extra params
// Diagnostics are not supported yet in the XCRemoteCache (cached artifacts assumes no warnings)
var diagnosticsPaths: [String]
// Unsed for now:
// .swiftsourceinfo and .swiftdoc will be placed next to the .swiftmodule
let sourceInfoPath: String?
let docPath: String?
// Passed as -supplementary-output-file-map
let supplementaryOutputFileMap: String?

/// Manual initializer implementation required to be public
public init(
compile: Bool,
emitModule: Bool,
objcHeaderOutput: String?,
moduleName: String?,
target: String?,
primaryInputPaths: [String],
inputPaths: [String],
outputPaths: [String],
dependenciesPaths: [String],
diagnosticsPaths: [String],
sourceInfoPath: String?,
docPath: String?,
supplementaryOutputFileMap: String?
) {
self.compile = compile
self.emitModule = emitModule
self.objcHeaderOutput = objcHeaderOutput
self.moduleName = moduleName
self.target = target
self.primaryInputPaths = primaryInputPaths
self.inputPaths = inputPaths
self.outputPaths = outputPaths
self.dependenciesPaths = dependenciesPaths
self.diagnosticsPaths = diagnosticsPaths
self.sourceInfoPath = sourceInfoPath
self.docPath = docPath
self.supplementaryOutputFileMap = supplementaryOutputFileMap
}

private func generateForCompilation(
config: XCRemoteCacheConfig,
target: String,
moduleName: String
) throws -> SwiftcContext {
let primaryInputsCount = primaryInputPaths.count

guard primaryInputsCount > 0 else {
throw SwiftFrontendArgInputError.noPrimaryFileCompilationInputs
}
guard [primaryInputsCount, 0].contains(dependenciesPaths.count) else {
throw SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(
expected: primaryInputsCount,
parsed: dependenciesPaths.count
)
}
guard [primaryInputsCount, 0].contains(diagnosticsPaths.count) else {
throw SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(
expected: primaryInputsCount,
parsed: diagnosticsPaths.count
)
}
guard outputPaths.count == primaryInputsCount else {
throw SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(
expected: primaryInputsCount,
parsed: outputPaths.count
)
}
let primaryInputFilesURLs: [URL] = primaryInputPaths.map(URL.init(fileURLWithPath:))

let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
compileFilesScope: .subset(primaryInputFilesURLs),
emitModule: nil
)

let compilationFilesInputs = buildCompilationFilesInputs(
primaryInputsCount: primaryInputsCount,
primaryInputFilesURLs: primaryInputFilesURLs
)

return try .init(
config: config,
moduleName: moduleName,
steps: steps,
inputs: compilationFilesInputs,
target: target,
compilationFiles: .list(inputPaths),
exampleWorkspaceFilePath: outputPaths[0]
)
}

private func buildCompilationFilesInputs(
primaryInputsCount: Int,
primaryInputFilesURLs: [URL]
) -> SwiftcContext.CompilationFilesInputs {
if let compimentaryFileMa = supplementaryOutputFileMap {
return .supplementaryFileMap(compimentaryFileMa)
} else {
return .map((0..<primaryInputsCount).reduce(
[String: SwiftFileCompilationInfo]()
) { prev, i in
var new = prev
new[primaryInputPaths[i]] = SwiftFileCompilationInfo(
file: primaryInputFilesURLs[i],
dependencies: dependenciesPaths.get(i).map(URL.init(fileURLWithPath:)),
object: outputPaths.get(i).map(URL.init(fileURLWithPath:)),
// for now - swift-dependencies are not requested in the driver compilation mode
swiftDependencies: nil
)
return new
})
}
}

private func generateForEmitModule(
config: XCRemoteCacheConfig,
target: String,
moduleName: String
) throws -> SwiftcContext {
guard outputPaths.count == 1 else {
throw SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: outputPaths.count)
}
guard let objcHeaderOutput = objcHeaderOutput else {
throw SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath
}
guard diagnosticsPaths.count <= 1 else {
throw SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(
parsed: diagnosticsPaths.count
)
}
guard dependenciesPaths.count <= 1 else {
throw SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(
parsed: dependenciesPaths.count
)
}

let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
compileFilesScope: .none,
emitModule: SwiftcContext.SwiftcStepEmitModule(
objcHeaderOutput: URL(fileURLWithPath: objcHeaderOutput),
modulePathOutput: URL(fileURLWithPath: outputPaths[0]),
dependencies: dependenciesPaths.first.map(URL.init(fileURLWithPath:))
)
)
return try .init(
config: config,
moduleName: moduleName,
steps: steps,
inputs: .map([:]),
target: target,
compilationFiles: .list(inputPaths),
exampleWorkspaceFilePath: objcHeaderOutput
)
}

func generateSwiftcContext(config: XCRemoteCacheConfig) throws -> SwiftcContext {
guard compile != emitModule else {
throw SwiftFrontendArgInputError.bothCompilationAndEmitAction
}
let inputPathsCount = inputPaths.count
guard inputPathsCount > 0 else {
throw SwiftFrontendArgInputError.noCompilationInputs
}
guard let target = target else {
throw SwiftFrontendArgInputError.emitMissingTarget
}
guard let moduleName = moduleName else {
throw SwiftFrontendArgInputError.emitMissingModuleName
}

if compile {
return try generateForCompilation(
config: config,
target: target,
moduleName: moduleName
)
} else {
return try generateForEmitModule(
config: config,
target: target,
moduleName: moduleName
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import Foundation

/// Manages the `swift-frontend` logic
protocol SwiftFrontendOrchestrator {
/// Executes the criticial secion according to the required order
/// - Parameter criticalSection: the block that should be synchronized
func run(criticalSection: () -> Void ) throws
}

/// The default orchestrator that manages the order or swift-frontend invocations
/// For emit-module (the "first" process) action, it locks a shared file between all swift-frontend invcations,
/// verifies that the mocking can be done and continues the mocking/fallbacking along the lock release
/// For the compilation action, tries to ackquire a lock and waits until the "emit-module" makes a decision
/// if the compilation should be skipped and a "mocking" should used instead
class CommonSwiftFrontendOrchestrator {
private let mode: SwiftcContext.SwiftcMode

init(mode: SwiftcContext.SwiftcMode) {
self.mode = mode
}

func run(criticalSection: () throws -> Void) throws {
// TODO: implement synchronization in a separate PR
try criticalSection()
}
}
Loading

0 comments on commit 65bf915

Please sign in to comment.