Sources/xcswift-frontend/XCSwiftcFrontendMain.swift (93 lines of code) (raw):
// 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
import XCRemoteCache
/// Wrapper for a `swift-frontend` that skips compilation and
/// produces empty output files (.o). Just like in xcswiftc, compilation dependencies
/// (.d) files are copied from the prebuild marker file which includes all relevant files
/// Fallbacks to a standard `swift-frontend` when the
/// ramote cache is not applicable (e.g. modified sources)
public class XCSwiftcFrontendMain {
// swiftlint:disable:next function_body_length cyclomatic_complexity
public func main() {
let env = ProcessInfo.processInfo.environment
// Do not invoke raw swift-frontend because that would lead to the infinite loop
// swift-frontent -> xcswift-frontent -> swift-frontent
//
// Note: Returning the `swiftc` executaion here because it is possible to pass all arguments
// from swift-frontend to `swiftc` and swiftc will be able to redirect to swift-frontend
// (because the first argument is `-frontend`). If that is not a case (might change in
// future swift compiler versions), invoke swift-frontend from the Xcode, but that introduces
// a limitation that disallows custom toolchains in Xcode:
// $DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/{ ProcessInfo().processName}
let command = "swiftc"
let args = ProcessInfo().arguments
var compile = false
var emitModule = false
var objcHeaderOutput: String?
var moduleName: String?
var target: String?
var inputPaths: [String] = []
var primaryInputPaths: [String] = []
var outputPaths: [String] = []
var dependenciesPaths: [String] = []
var diagnosticsPaths: [String] = []
var sourceInfoPath: String?
var docPath: String?
var supplementaryOutputFileMap: String?
for i in 0..<args.count {
let arg = args[i]
switch arg {
case "-c":
compile = true
case "-emit-module":
emitModule = true
case "-o":
outputPaths.append(args[i + 1])
case "-emit-objc-header-path":
objcHeaderOutput = args[i + 1]
case "-module-name":
moduleName = args[i + 1]
case "-target":
target = args[i + 1]
case "-serialize-diagnostics-path":
// .dia
diagnosticsPaths.append(args[i + 1])
case "-emit-dependencies-path":
// .d
dependenciesPaths.append(args[i + 1])
case "-emit-module-source-info-path":
// .swiftsourceinfo
sourceInfoPath = args[i + 1]
case "-emit-module-doc-path":
// .swiftdoc
docPath = args[i + 1]
case "-primary-file":
// .swift
primaryInputPaths.append(args[i + 1])
case "-supplementary-output-file-map":
supplementaryOutputFileMap = args[i + 1]
default:
if arg.hasSuffix(".swift") {
inputPaths.append(arg)
}
}
}
// support either emitModule (the preflight step) or compilation
// all other types of invocations (like -print-target-info) should be
// automatically redirected to the original swift-frontend
let argInput = SwiftFrontendArgInput(
compile: compile,
emitModule: emitModule,
objcHeaderOutput: objcHeaderOutput,
moduleName: moduleName,
target: target,
primaryInputPaths: primaryInputPaths,
inputPaths: inputPaths,
outputPaths: outputPaths,
dependenciesPaths: dependenciesPaths,
diagnosticsPaths: diagnosticsPaths,
sourceInfoPath: sourceInfoPath,
docPath: docPath,
supplementaryOutputFileMap: supplementaryOutputFileMap
)
// swift-frontend is first invoked with some "probing" args like
// -print-target-info
guard emitModule != compile else {
runFallback(envs: env)
}
do {
let frontend = try XCSwiftFrontend(
command: command,
inputArgs: argInput,
env: env,
dependenciesWriter: FileDependenciesWriter.init,
touchFactory: FileTouch.init)
try frontend.run()
} catch {
runFallback(envs: env)
}
}
private func runFallback(envs env: [String: String]) -> Never {
// DEVELOPER_DIR is always set by Xcode
let developerDir = env["DEVELOPER_DIR"]!
// limitation: always using the Xcode's toolchain, otherwise
// there will be a loop for invoking swift-frontend wrapper from XCRemoteCache
// Cause: for injecting into the swift driver pipeline, Xcode looks for
// an executable with the name `swift-frontend` that is placed in the same
// dir as `SWIFT_EXEC`'s `swiftc` wrapper
let swiftFrontendCommand = "\(developerDir)/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend"
let args = ProcessInfo().arguments
let paramList = [swiftFrontendCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(swiftFrontendCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
}