Swift: rework file redirection

The hash map mechanism that was already in use for reading swiftmodule
files on macOS is now in use also on Linux. The output replacing
mechanism has been also reworked so that:
* frontend module emission modes have the remapping done directly in
  the internal frontend options instead of painstakingly modifying input
  flags (this requires a patch on the swift headers though)
* object emission mode is silenced to be just a type checking pass,
  thus producing no output files
* all other passes but some debugging and version related ones become
  noops

The open file read redirection uses a global weak pointer instance to
maximize robustness in the face of possibly multi-threaded calls to open
happening while `main` is exiting. Possibly overkill, but better safe
than sorry.
This commit is contained in:
Paolo Tranquilli
2022-12-05 17:22:45 +01:00
parent 944adfe727
commit bf1b32f210
18 changed files with 263 additions and 333 deletions

View File

@@ -9,6 +9,7 @@ swift_cc_binary(
]),
visibility = ["//swift:__pkg__"],
deps = [
"//swift/extractor/config",
"//swift/extractor/infra",
"//swift/extractor/invocation",
"//swift/extractor/remapping",

View File

@@ -1,6 +1,6 @@
#pragma once
#include "swift/extractor/SwiftExtractorConfiguration.h"
#include "swift/extractor/config/SwiftExtractorConfiguration.h"
#include <swift/AST/SourceFile.h>
#include <swift/Frontend/Frontend.h>
#include <memory>

View File

@@ -11,11 +11,6 @@ std::optional<TargetFile> createTargetTrapFile(const SwiftExtractorConfiguration
for (const auto& opt : configuration.frontendOptions) {
*ret << " " << std::quoted(opt) << " \\\n";
}
*ret << "\n*/\n"
"/* swift-frontend-args:\n";
for (const auto& opt : configuration.patchedFrontendOptions) {
*ret << " " << std::quoted(opt) << " \\\n";
}
*ret << "\n*/\n";
}
return ret;

View File

@@ -1,7 +1,7 @@
#pragma once
#include "swift/extractor/infra/file/TargetFile.h"
#include "swift/extractor/SwiftExtractorConfiguration.h"
#include "swift/extractor/config/SwiftExtractorConfiguration.h"
namespace codeql {

View File

@@ -0,0 +1,8 @@
load("//swift:rules.bzl", "swift_cc_library")
swift_cc_library(
name = "config",
srcs = glob(["*.cpp"]),
hdrs = glob(["*.h"]),
visibility = ["//swift:__subpackages__"],
)

View File

@@ -17,9 +17,7 @@ struct SwiftExtractorConfiguration {
std::filesystem::path scratchDir;
// The original arguments passed to the extractor. Used for debugging.
std::vector<std::string> frontendOptions;
// The patched arguments passed to the swift::performFrontend/ Used for debugging.
std::vector<std::string> patchedFrontendOptions;
std::vector<const char*> frontendOptions;
// A temporary directory that contains TRAP files before they are moved into their final
// destination.

View File

@@ -12,13 +12,58 @@
#include "swift/extractor/SwiftExtractor.h"
#include "swift/extractor/TargetTrapFile.h"
#include "swift/extractor/remapping/SwiftOutputRewrite.h"
#include "swift/extractor/remapping/SwiftOpenInterception.h"
#include "swift/extractor/remapping/SwiftFileInterception.h"
#include "swift/extractor/invocation/SwiftDiagnosticsConsumer.h"
#include "swift/extractor/trap/TrapDomain.h"
using namespace std::string_literals;
static void lockOutputSwiftModuleTraps(const codeql::SwiftExtractorConfiguration& config,
const swift::CompilerInstance& compiler) {
std::filesystem::path output = compiler.getInvocation().getOutputFilename();
if (output.extension() == ".swiftmodule") {
if (auto target = codeql::createTargetTrapFile(config, output)) {
*target << "// trap file deliberately empty\n"
"// this swiftmodule was created during the build, so its entities must have"
" been extracted directly from source files";
}
}
}
static void modifyFrontendOptions(swift::FrontendOptions& options) {
using Action = swift::FrontendOptions::ActionType;
switch (options.RequestedAction) {
case Action::EmitModuleOnly:
case Action::MergeModules:
case Action::CompileModuleFromInterface:
// for module emission actions, we redirect the output to our internal artifact storage
{
swift::SupplementaryOutputPaths paths;
paths.ModuleOutputPath =
codeql::redirect(options.InputsAndOutputs.getSingleOutputFilename()).string();
options.InputsAndOutputs.setMainAndSupplementaryOutputs(std::vector{paths.ModuleOutputPath},
std::vector{paths});
return;
}
case Action::EmitObject:
// for object emission, we do a type check pass instead, muting output but getting the sema
// phase to run in order to extract everything
options.RequestedAction = Action::Typecheck;
return;
case Action::PrintVersion:
case Action::DumpAST:
case Action::PrintAST:
case Action::PrintASTDecl:
// these actions are nice to have on the extractor for debugging, so we preserve them. Also,
// version printing is used by CI to match up the correct compiler version
return;
default:
// otherwise, do nothing (the closest action to doing nothing is printing the version)
options.RequestedAction = Action::PrintVersion;
break;
}
}
// This is part of the swiftFrontendTool interface, we hook into the
// compilation pipeline and extract files after the Swift frontend performed
// semantic analysis
@@ -28,8 +73,13 @@ class Observer : public swift::FrontendObserver {
codeql::SwiftDiagnosticsConsumer& diagConsumer)
: config{config}, diagConsumer{diagConsumer} {}
void parsedArgs(swift::CompilerInvocation& invocation) override {
modifyFrontendOptions(invocation.getFrontendOptions());
}
void configuredCompiler(swift::CompilerInstance& instance) override {
instance.addDiagnosticConsumer(&diagConsumer);
lockOutputSwiftModuleTraps(config, instance);
}
void performedSemanticAnalysis(swift::CompilerInstance& compiler) override {
@@ -48,19 +98,6 @@ static std::string getenv_or(const char* envvar, const std::string& def) {
return def;
}
static void lockOutputSwiftModuleTraps(const codeql::SwiftExtractorConfiguration& config,
const codeql::PathRemapping& remapping) {
for (const auto& [oldPath, newPath] : remapping) {
if (oldPath.extension() == ".swiftmodule") {
if (auto target = codeql::createTargetTrapFile(config, oldPath)) {
*target << "// trap file deliberately empty\n"
"// this swiftmodule was created during the build, so its entities must have"
" been extracted directly from source files";
}
}
}
}
static bool checkRunUnderFilter(int argc, char* const* argv) {
auto runUnderFilter = getenv("CODEQL_EXTRACTOR_SWIFT_RUN_UNDER_FILTER");
if (runUnderFilter == nullptr) {
@@ -106,7 +143,7 @@ static void checkWhetherToRunUnderTool(int argc, char* const* argv) {
// Creates a target file that should store per-invocation info, e.g. compilation args,
// compilations, diagnostics, etc.
codeql::TargetFile invocationTargetFile(codeql::SwiftExtractorConfiguration& configuration) {
codeql::TargetFile invocationTargetFile(const codeql::SwiftExtractorConfiguration& configuration) {
auto timestamp = std::chrono::system_clock::now().time_since_epoch().count();
auto filename = std::to_string(timestamp) + '-' + std::to_string(getpid());
auto target = std::filesystem::path("invocations") / std::filesystem::path(filename);
@@ -118,6 +155,15 @@ codeql::TargetFile invocationTargetFile(codeql::SwiftExtractorConfiguration& con
return std::move(maybeFile.value());
}
codeql::SwiftExtractorConfiguration configure(int argc, char** argv) {
codeql::SwiftExtractorConfiguration configuration{};
configuration.trapDir = getenv_or("CODEQL_EXTRACTOR_SWIFT_TRAP_DIR", ".");
configuration.sourceArchiveDir = getenv_or("CODEQL_EXTRACTOR_SWIFT_SOURCE_ARCHIVE_DIR", ".");
configuration.scratchDir = getenv_or("CODEQL_EXTRACTOR_SWIFT_SCRATCH_DIR", ".");
configuration.frontendOptions.assign(argv + 1, argv + argc);
return configuration;
}
int main(int argc, char** argv) {
checkWhetherToRunUnderTool(argc, argv);
@@ -130,36 +176,16 @@ int main(int argc, char** argv) {
PROGRAM_START(argc, argv);
INITIALIZE_LLVM();
codeql::SwiftExtractorConfiguration configuration{};
configuration.trapDir = getenv_or("CODEQL_EXTRACTOR_SWIFT_TRAP_DIR", ".");
configuration.sourceArchiveDir = getenv_or("CODEQL_EXTRACTOR_SWIFT_SOURCE_ARCHIVE_DIR", ".");
configuration.scratchDir = getenv_or("CODEQL_EXTRACTOR_SWIFT_SCRATCH_DIR", ".");
const auto configuration = configure(argc, argv);
codeql::initRemapping(configuration.getTempArtifactDir());
configuration.frontendOptions.reserve(argc - 1);
for (int i = 1; i < argc; i++) {
configuration.frontendOptions.push_back(argv[i]);
}
configuration.patchedFrontendOptions = configuration.frontendOptions;
auto remapping = codeql::rewriteOutputsInPlace(configuration.getTempArtifactDir(),
configuration.patchedFrontendOptions);
codeql::ensureDirectoriesForNewPathsExist(remapping);
lockOutputSwiftModuleTraps(configuration, remapping);
std::vector<const char*> args;
for (auto& arg : configuration.patchedFrontendOptions) {
args.push_back(arg.c_str());
}
auto openInterception = codeql::setupFileInterception(configuration.getTempArtifactDir());
auto invocationTrapFile = invocationTargetFile(configuration);
codeql::TrapDomain invocationDomain(invocationTrapFile);
codeql::SwiftDiagnosticsConsumer diagConsumer(invocationDomain);
Observer observer(configuration, diagConsumer);
int frontend_rc = swift::performFrontend(args, "swift-extractor", (void*)main, &observer);
codeql::finalizeRemapping(remapping);
int frontend_rc = swift::performFrontend(configuration.frontendOptions, "swift-extractor",
(void*)main, &observer);
return frontend_rc;
}

View File

@@ -2,23 +2,11 @@ load("//swift:rules.bzl", "swift_cc_library")
swift_cc_library(
name = "remapping",
srcs = ["SwiftOutputRewrite.cpp"] + select({
"@platforms//os:linux": [
"SwiftOpenInterception.Linux.cpp",
],
"@platforms//os:macos": [
"SwiftOpenInterception.macOS.cpp",
],
}),
srcs = glob(["*.cpp"]),
hdrs = glob(["*.h"]),
visibility = ["//swift:__subpackages__"],
deps = [
"//swift/third_party/swift-llvm-support",
"//swift/extractor/infra/file",
] + select({
"@platforms//os:linux": [],
"@platforms//os:macos": [
"@fishhook//:fishhook",
],
}),
"//swift/third_party/swift-llvm-support",
],
)

View File

@@ -0,0 +1,160 @@
#include "swift/extractor/remapping/SwiftFileInterception.h"
#include <fcntl.h>
#include <filesystem>
#include <dlfcn.h>
#include <mutex>
#include <optional>
#include <cassert>
#include "swift/extractor/infra/file/FileHash.h"
#include "swift/extractor/infra/file/FileHash.h"
#ifdef __APPLE__
#define SHARED_LIBC "libc.dylib"
#define FILE_CREATION_MODE O_CREAT
#else
#define SHARED_LIBC "libc.so.6"
#define FILE_CREATION_MODE (O_CREAT | O_TMPFILE)
#endif
namespace fs = std::filesystem;
namespace {
namespace original {
void* libc() {
static auto ret = dlopen(SHARED_LIBC, RTLD_LAZY);
return ret;
}
template <typename Signature>
Signature get(const char* name) {
return reinterpret_cast<Signature>(dlsym(libc(), name));
}
int open(const char* path, int flags, mode_t mode = 0) {
static auto original = get<int (*)(const char*, int, ...)>("open");
return original(path, flags, mode);
}
} // namespace original
bool endsWith(const std::string_view& s, const std::string_view& suffix) {
return s.size() >= suffix.size() && s.substr(s.size() - suffix.size()) == suffix;
}
auto& fileInterceptorInstance() {
static std::weak_ptr<codeql::FileInterceptor> ret{};
return ret;
}
bool mayBeRedirected(const char* path, int flags = O_RDONLY) {
return (!fileInterceptorInstance().expired() && (flags & O_ACCMODE) == O_RDONLY &&
endsWith(path, ".swiftmodule"));
}
} // namespace
namespace codeql {
class FileInterceptor {
public:
FileInterceptor(fs::path&& workingDir) : workingDir{std::move(workingDir)} {
fs::create_directories(hashesPath());
fs::create_directories(storePath());
}
int open(const char* path, int flags, mode_t mode = 0) const {
fs::path fsPath{path};
assert((flags & O_ACCMODE) == O_RDONLY);
errno = 0;
// first, try the same path underneath the artifact store
if (auto ret = original::open(redirectedPath(path).c_str(), flags);
ret >= 0 || errno != ENOENT) {
return ret;
}
errno = 0;
// then try to use the hash map
if (auto hashed = hashPath(path)) {
if (auto ret = original::open(hashed->c_str(), flags); ret >= 0 || errno != ENOENT) {
return ret;
}
}
return original::open(path, flags, mode);
}
fs::path redirect(const fs::path& target) const {
assert(mayBeRedirected(target.c_str()));
auto ret = redirectedPath(target);
fs::create_directories(ret.parent_path());
if (auto hashed = hashPath(target)) {
std::error_code ec;
fs::create_symlink(ret, *hashed, ec);
if (ec) {
std::cerr << "Cannot remap file " << ret << " -> " << *hashed << ": " << ec.message()
<< "\n";
}
}
return ret;
}
private:
fs::path hashesPath() const { return workingDir / "hashes"; }
fs::path storePath() const { return workingDir / "store"; }
fs::path redirectedPath(const fs::path& target) const {
return storePath() / target.relative_path();
}
std::optional<fs::path> hashPath(const fs::path& target) const {
if (auto fd = original::open(target.c_str(), O_RDONLY | O_CLOEXEC); fd >= 0) {
return hashesPath() / hashFile(fd);
}
return std::nullopt;
}
fs::path workingDir;
};
int openReal(const fs::path& path) {
return original::open(path.c_str(), O_RDONLY | O_CLOEXEC);
}
fs::path redirect(const fs::path& target) {
if (auto interceptor = fileInterceptorInstance().lock()) {
return interceptor->redirect(target);
} else {
return target;
}
}
std::shared_ptr<FileInterceptor> setupFileInterception(fs::path workginDir) {
auto ret = std::make_shared<FileInterceptor>(std::move(workginDir));
fileInterceptorInstance() = ret;
return ret;
}
} // namespace codeql
extern "C" {
int open(const char* path, int flags, ...) {
mode_t mode = 0;
if (flags & FILE_CREATION_MODE) {
va_list ap;
// mode only applies when creating a file
va_start(ap, flags);
mode = va_arg(ap, int);
va_end(ap);
}
if (mayBeRedirected(path, flags)) {
if (auto interceptor = fileInterceptorInstance().lock()) {
return interceptor->open(path, flags, mode);
}
}
return original::open(path, flags, mode);
}
} // namespace codeql

View File

@@ -0,0 +1,19 @@
#pragma once
#include <string>
#include <unordered_map>
#include <filesystem>
#include <memory>
#include "swift/extractor/infra/file/PathHash.h"
namespace codeql {
int openReal(const std::filesystem::path& path);
class FileInterceptor;
std::shared_ptr<FileInterceptor> setupFileInterception(std::filesystem::path workingDir);
std::filesystem::path redirect(const std::filesystem::path& target);
} // namespace codeql

View File

@@ -1,9 +0,0 @@
#include "swift/extractor/remapping/SwiftOpenInterception.h"
namespace codeql {
// TBD
void initRemapping(const std::filesystem::path& dir) {}
void finalizeRemapping(
const std::unordered_map<std::filesystem::path, std::filesystem::path>& mapping) {}
} // namespace codeql

View File

@@ -1,15 +0,0 @@
#pragma once
#include <string>
#include <unordered_map>
#include <filesystem>
#include "swift/extractor/infra/file/PathHash.h"
namespace codeql {
void initRemapping(const std::filesystem::path& dir);
void finalizeRemapping(
const std::unordered_map<std::filesystem::path, std::filesystem::path>& mapping);
} // namespace codeql

View File

@@ -1,85 +0,0 @@
#include "swift/extractor/remapping/SwiftOpenInterception.h"
#include <fishhook.h>
#include <fcntl.h>
#include <unistd.h>
#include <filesystem>
#include "swift/extractor/infra/file/FileHash.h"
namespace fs = std::filesystem;
namespace codeql {
static fs::path scratchDir;
static bool interceptionEnabled = false;
static int (*original_open)(const char*, int, ...) = nullptr;
static std::string originalHashFile(const fs::path& filename) {
int fd = original_open(filename.c_str(), O_RDONLY);
if (fd == -1) {
return {};
}
return hashFile(fd);
}
static int codeql_open(const char* path, int oflag, ...) {
va_list ap;
mode_t mode = 0;
if ((oflag & O_CREAT) != 0) {
// mode only applies to O_CREAT
va_start(ap, oflag);
mode = va_arg(ap, int);
va_end(ap);
}
fs::path newPath(path);
if (interceptionEnabled && fs::exists(newPath)) {
// TODO: check file magic instead
if (newPath.extension() == ".swiftmodule") {
auto hash = originalHashFile(newPath);
auto hashed = scratchDir / hash;
if (!hash.empty() && fs::exists(hashed)) {
newPath = hashed;
}
}
}
return original_open(newPath.c_str(), oflag, mode);
}
void finalizeRemapping(
const std::unordered_map<std::filesystem::path, std::filesystem::path>& mapping) {
for (auto& [original, patched] : mapping) {
// TODO: Check file magic instead
if (original.extension() != ".swiftmodule") {
continue;
}
auto hash = originalHashFile(original);
auto hashed = scratchDir / hash;
if (!hash.empty() && fs::exists(patched)) {
std::error_code ec;
fs::create_symlink(/* target */ patched, /* symlink */ hashed, ec);
if (ec) {
std::cerr << "Cannot remap file '" << patched << "' -> '" << hashed << "': " << ec.message()
<< "\n";
}
}
}
interceptionEnabled = false;
}
void initRemapping(const std::filesystem::path& dir) {
scratchDir = dir;
struct rebinding binding[] = {
{"open", reinterpret_cast<void*>(codeql_open), reinterpret_cast<void**>(&original_open)}};
rebind_symbols(binding, 1);
interceptionEnabled = true;
}
} // namespace codeql

View File

@@ -1,137 +0,0 @@
#include "swift/extractor/remapping/SwiftOutputRewrite.h"
#include <llvm/ADT/SmallString.h>
#include <swift/Basic/OutputFileMap.h>
#include <swift/Basic/Platform.h>
#include <unistd.h>
#include <unordered_set>
#include <optional>
#include <iostream>
#include "swift/extractor/infra/file/PathHash.h"
namespace fs = std::filesystem;
namespace codeql {
// Creates a copy of the output file map and updates remapping table in place
// It does not change the original map file as it is depended upon by the original compiler
// Returns path to the newly created output file map on success, or None in a case of failure
static std::optional<fs::path> rewriteOutputFileMap(const fs::path& scratchDir,
const fs::path& outputFileMapPath,
const std::vector<fs::path>& inputs,
PathRemapping& remapping) {
auto newMapPath = scratchDir / outputFileMapPath.relative_path();
// TODO: do not assume absolute path for the second parameter
auto outputMapOrError = swift::OutputFileMap::loadFromPath(outputFileMapPath.c_str(), "");
if (!outputMapOrError) {
std::cerr << "Cannot load output map " << outputFileMapPath << "\n";
return std::nullopt;
}
auto oldOutputMap = outputMapOrError.get();
swift::OutputFileMap newOutputMap;
std::vector<llvm::StringRef> keys;
for (auto& key : inputs) {
auto oldMap = oldOutputMap.getOutputMapForInput(key.c_str());
if (!oldMap) {
continue;
}
keys.push_back(key.c_str());
auto& newMap = newOutputMap.getOrCreateOutputMapForInput(key.c_str());
newMap.copyFrom(*oldMap);
for (auto& entry : newMap) {
fs::path oldPath = entry.getSecond();
auto newPath = scratchDir / oldPath.relative_path();
entry.getSecond() = newPath;
remapping[oldPath] = newPath;
}
}
std::error_code ec;
fs::create_directories(newMapPath.parent_path(), ec);
if (ec) {
std::cerr << "Cannot create relocated output map dir " << newMapPath.parent_path() << ": "
<< ec.message() << "\n";
return std::nullopt;
}
llvm::raw_fd_ostream fd(newMapPath.c_str(), ec, llvm::sys::fs::OF_None);
newOutputMap.write(fd, keys);
return newMapPath;
}
PathRemapping rewriteOutputsInPlace(const fs::path& scratchDir, std::vector<std::string>& CLIArgs) {
PathRemapping remapping;
// TODO: handle filelists?
const std::unordered_set<std::string> pathRewriteOptions({
"-emit-abi-descriptor-path",
"-emit-dependencies-path",
"-emit-module-path",
"-emit-module-doc-path",
"-emit-module-source-info-path",
"-emit-objc-header-path",
"-emit-reference-dependencies-path",
"-index-store-path",
"-index-unit-output-path",
"-module-cache-path",
"-o",
"-pch-output-dir",
"-serialize-diagnostics-path",
});
std::unordered_set<std::string> outputFileMaps(
{"-supplementary-output-file-map", "-output-file-map"});
std::vector<size_t> outputFileMapIndexes;
std::vector<fs::path> maybeInput;
std::string targetTriple;
std::vector<fs::path> newLocations;
for (size_t i = 0; i < CLIArgs.size(); i++) {
if (pathRewriteOptions.count(CLIArgs[i])) {
fs::path oldPath = CLIArgs[i + 1];
auto newPath = scratchDir / oldPath.relative_path();
CLIArgs[++i] = newPath.string();
newLocations.push_back(newPath);
remapping[oldPath] = newPath;
} else if (outputFileMaps.count(CLIArgs[i])) {
// collect output map indexes for further rewriting and skip the following argument
// We don't patch the map in place as we need to collect all the input files first
outputFileMapIndexes.push_back(++i);
} else if (CLIArgs[i] == "-target") {
targetTriple = CLIArgs[++i];
} else if (CLIArgs[i][0] != '-') {
// TODO: add support for input file lists?
// We need to collect input file names to later use them to extract information from the
// output file maps.
maybeInput.push_back(CLIArgs[i]);
}
}
for (auto index : outputFileMapIndexes) {
auto oldPath = CLIArgs[index];
auto maybeNewPath = rewriteOutputFileMap(scratchDir, oldPath, maybeInput, remapping);
if (maybeNewPath) {
const auto& newPath = maybeNewPath.value();
CLIArgs[index] = newPath;
remapping[oldPath] = newPath;
}
}
return remapping;
}
void ensureDirectoriesForNewPathsExist(const PathRemapping& remapping) {
for (auto& [_, newPath] : remapping) {
std::error_code ec;
fs::create_directories(newPath.parent_path(), ec);
if (ec) {
std::cerr << "Cannot create redirected directory " << newPath.parent_path() << ": "
<< ec.message() << "\n";
}
}
}
} // namespace codeql

View File

@@ -1,21 +0,0 @@
#pragma once
#include <vector>
#include <string>
#include <unordered_map>
#include "swift/extractor/infra/file/PathHash.h"
namespace codeql {
using PathRemapping = std::unordered_map<std::filesystem::path, std::filesystem::path>;
// Rewrites all the output CLI args to point to a scratch dir instead of the actual locations.
// This is needed to ensure that the artifacts produced by the extractor do not collide with the
// artifacts produced by the actual Swift compiler.
// Returns the map containing remapping oldpath -> newPath.
PathRemapping rewriteOutputsInPlace(const std::filesystem::path& scratchDir,
std::vector<std::string>& CLIArgs);
// Create directories for all the redirected new paths as the Swift compiler expects them to exist.
void ensureDirectoriesForNewPathsExist(const PathRemapping& remapping);
} // namespace codeql

View File

@@ -0,0 +1 @@
func foo() {}

View File

@@ -0,0 +1 @@
func bar() {}

View File

@@ -15,4 +15,4 @@ $FRONTEND -frontend -c -primary-file D.swift -o D.o $SDK
$FRONTEND -frontend -c -primary-file E.swift Esup.swift -o E.o $SDK
$FRONTEND -frontend -emit-module -primary-file F1.swift F2.swift -module-name F -o F1.swiftmodule $SDK
$FRONTEND -frontend -emit-module F1.swift -primary-file F2.swift -module-name F -o F2.swiftmodule $SDK
$FRONTEND -merge-modules F1.swiftmodule F2.swiftmodule -o F.swiftmodule $SDK
$FRONTEND -frontend -merge-modules F1.swiftmodule F2.swiftmodule -o F.swiftmodule $SDK