Skip to content

Commit 4367044

Browse files
authored
Merge pull request #9702 from github/alexdenisov/swift-multiple-modules
Swift: emit intermediate build artifacts for own consumption later
2 parents de9e885 + d42b752 commit 4367044

File tree

7 files changed

+415
-15
lines changed

7 files changed

+415
-15
lines changed

swift/extractor/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ load("//swift:rules.bzl", "swift_cc_binary")
33
swift_cc_binary(
44
name = "extractor",
55
srcs = [
6+
"SwiftOutputRewrite.cpp",
7+
"SwiftOutputRewrite.h",
68
"SwiftExtractor.cpp",
79
"SwiftExtractor.h",
810
"SwiftExtractorConfiguration.h",

swift/extractor/SwiftExtractor.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,17 @@ static void extractDeclarations(const SwiftExtractorConfiguration& config,
9494
<< "': " << ec.message() << "\n";
9595
return;
9696
}
97-
trapStream << "// extractor-args: ";
97+
trapStream << "/* extractor-args:\n";
9898
for (auto opt : config.frontendOptions) {
99-
trapStream << std::quoted(opt) << " ";
99+
trapStream << " " << std::quoted(opt) << " \\\n";
100100
}
101-
trapStream << "\n\n";
101+
trapStream << "\n*/\n";
102+
103+
trapStream << "/* swift-frontend-args:\n";
104+
for (auto opt : config.patchedFrontendOptions) {
105+
trapStream << " " << std::quoted(opt) << " \\\n";
106+
}
107+
trapStream << "\n*/\n";
102108

103109
TrapOutput trap{trapStream};
104110
TrapArena arena{};

swift/extractor/SwiftExtractorConfiguration.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,22 @@ struct SwiftExtractorConfiguration {
1616
// Subdirectory of the scratchDir.
1717
std::string tempTrapDir;
1818

19+
// VFS (virtual file system) support.
20+
// A temporary directory that contains VFS files used during extraction.
21+
// Subdirectory of the scratchDir.
22+
std::string VFSDir;
23+
// A temporary directory that contains temp VFS files before they moved into VFSDir.
24+
// Subdirectory of the scratchDir.
25+
std::string tempVFSDir;
26+
27+
// A temporary directory that contains build artifacts generated by the extractor during the
28+
// overall extraction process.
29+
// Subdirectory of the scratchDir.
30+
std::string tempArtifactDir;
31+
1932
// The original arguments passed to the extractor. Used for debugging.
2033
std::vector<std::string> frontendOptions;
34+
// The patched arguments passed to the swift::performFrontend/ Used for debugging.
35+
std::vector<std::string> patchedFrontendOptions;
2136
};
2237
} // namespace codeql
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
#include "SwiftOutputRewrite.h"
2+
#include "swift/extractor/SwiftExtractorConfiguration.h"
3+
4+
#include <llvm/ADT/SmallString.h>
5+
#include <llvm/Support/FileSystem.h>
6+
#include <llvm/Support/Path.h>
7+
#include <swift/Basic/OutputFileMap.h>
8+
#include <swift/Basic/Platform.h>
9+
#include <unistd.h>
10+
#include <unordered_set>
11+
#include <optional>
12+
#include <iostream>
13+
14+
// Creates a copy of the output file map and updates remapping table in place
15+
// It does not change the original map file as it is depended upon by the original compiler
16+
// Returns path to the newly created output file map on success, or None in a case of failure
17+
static std::optional<std::string> rewriteOutputFileMap(
18+
const codeql::SwiftExtractorConfiguration& config,
19+
const std::string& outputFileMapPath,
20+
const std::vector<std::string>& inputs,
21+
std::unordered_map<std::string, std::string>& remapping) {
22+
auto newPath = config.tempArtifactDir + '/' + outputFileMapPath;
23+
24+
// TODO: do not assume absolute path for the second parameter
25+
auto outputMapOrError = swift::OutputFileMap::loadFromPath(outputFileMapPath, "");
26+
if (!outputMapOrError) {
27+
return std::nullopt;
28+
}
29+
auto oldOutputMap = outputMapOrError.get();
30+
swift::OutputFileMap newOutputMap;
31+
std::vector<llvm::StringRef> keys;
32+
for (auto& key : inputs) {
33+
auto oldMap = oldOutputMap.getOutputMapForInput(key);
34+
if (!oldMap) {
35+
continue;
36+
}
37+
keys.push_back(key);
38+
auto& newMap = newOutputMap.getOrCreateOutputMapForInput(key);
39+
newMap.copyFrom(*oldMap);
40+
for (auto& entry : newMap) {
41+
auto oldPath = entry.getSecond();
42+
auto newPath = config.tempArtifactDir + '/' + oldPath;
43+
entry.getSecond() = newPath;
44+
remapping[oldPath] = newPath;
45+
}
46+
}
47+
std::error_code ec;
48+
llvm::SmallString<PATH_MAX> filepath(newPath);
49+
llvm::StringRef parent = llvm::sys::path::parent_path(filepath);
50+
if (std::error_code ec = llvm::sys::fs::create_directories(parent)) {
51+
std::cerr << "Cannot create relocated output map dir: '" << parent.str()
52+
<< "': " << ec.message() << "\n";
53+
return std::nullopt;
54+
}
55+
56+
llvm::raw_fd_ostream fd(newPath, ec, llvm::sys::fs::OF_None);
57+
newOutputMap.write(fd, keys);
58+
return newPath;
59+
}
60+
61+
// This is an Xcode-specific workaround to produce alias names for an existing .swiftmodule file.
62+
// In the case of Xcode, it calls the Swift compiler and asks it to produce a Swift module.
63+
// Once it's done, Xcode moves the .swiftmodule file in another location, and the location is
64+
// rather arbitrary. Here are examples of such locations:
65+
// Original file produced by the frontend:
66+
// DerivedData/<Project>/Build/Intermediates.noindex/<Project>.build/<BuiltType>-<Target>/<Project>.build/Objects-normal/<Arch>/<ModuleName>.swiftmodule
67+
// where:
68+
// Project: name of a project, target, or scheme
69+
// BuildType: Debug, Release, etc.
70+
// Target: macOS, iphoneos, appletvsimulator, etc.
71+
// Arch: arm64, x86_64, etc.
72+
//
73+
// So far we observed that Xcode can move the module into different locations, and it's not
74+
// entirely clear how to deduce the destination from the context available for the extractor.
75+
// 1. First case:
76+
// DerivedData/<Project>/Build/Products/<BuiltType>-<Target>/<ModuleName>.swiftmodule/<Arch>.swiftmodule
77+
// DerivedData/<Project>/Build/Products/<BuiltType>-<Target>/<ModuleName>.swiftmodule/<Triple>.swiftmodule
78+
// 2. Second case:
79+
// DerivedData/<Project>/Build/Products/<BuiltType>-<Target>/<ModuleName>/<ModuleName>.swiftmodule/<Arch>.swiftmodule
80+
// DerivedData/<Project>/Build/Products/<BuiltType>-<Target>/<ModuleName>/<ModuleName>.swiftmodule/<Triple>.swiftmodule
81+
// 2. Third case:
82+
// DerivedData/<Project>/Build/Products/<BuiltType>-<Target>/<ModuleName>/<ModuleName>.framework/Modules/<ModuleName>.swiftmodule/<Arch>.swiftmodule
83+
// DerivedData/<Project>/Build/Products/<BuiltType>-<Target>/<ModuleName>/<ModuleName>.framework/Modules/<ModuleName>.swiftmodule/<Triple>.swiftmodule
84+
// The <Triple> here is a normalized target triple (e.g. arm64-apple-iphoneos15.4 ->
85+
// arm64-apple-iphoneos).
86+
//
87+
// This method constructs those aliases for a module only if it comes from Xcode, which is detected
88+
// by the presence of an `Intermediates.noindex` directory in the module path.
89+
//
90+
// In the case of the Swift Package Manager (`swift build`) this is not needed.
91+
static std::vector<std::string> computeModuleAliases(llvm::StringRef modulePath,
92+
const std::string& targetTriple) {
93+
if (modulePath.empty()) {
94+
return {};
95+
}
96+
if (!modulePath.endswith(".swiftmodule")) {
97+
return {};
98+
}
99+
// Deconstructing the Xcode generated path
100+
//
101+
// clang-format off
102+
// intermediatesDirIndex destinationDir (2) arch(5)
103+
// DerivedData/FooBar/Build/Intermediates.noindex/FooBar.build/Debug-iphonesimulator/FooBar.build/Objects-normal/x86_64/FooBar.swiftmodule
104+
// clang-format on
105+
llvm::SmallVector<llvm::StringRef> chunks;
106+
modulePath.split(chunks, '/');
107+
size_t intermediatesDirIndex = 0;
108+
for (size_t i = 0; i < chunks.size(); i++) {
109+
if (chunks[i] == "Intermediates.noindex") {
110+
intermediatesDirIndex = i;
111+
break;
112+
}
113+
}
114+
// Not built by Xcode, skipping
115+
if (intermediatesDirIndex == 0) {
116+
return {};
117+
}
118+
size_t destinationDirIndex = intermediatesDirIndex + 2;
119+
size_t archIndex = intermediatesDirIndex + 5;
120+
if (archIndex >= chunks.size()) {
121+
return {};
122+
}
123+
// e.g. Debug-iphoneos, Release-iphonesimulator, etc.
124+
auto destinationDir = chunks[destinationDirIndex].str();
125+
auto arch = chunks[archIndex].str();
126+
auto moduleNameWithExt = chunks.back();
127+
auto moduleName = moduleNameWithExt.substr(0, moduleNameWithExt.find_last_of('.'));
128+
std::string relocatedModulePath = chunks[0].str();
129+
for (size_t i = 1; i < intermediatesDirIndex; i++) {
130+
relocatedModulePath += '/' + chunks[i].str();
131+
}
132+
relocatedModulePath += "/Products/";
133+
relocatedModulePath += destinationDir + '/';
134+
135+
std::vector<std::string> moduleLocations;
136+
137+
std::string firstCase = relocatedModulePath;
138+
firstCase += moduleNameWithExt.str() + '/';
139+
moduleLocations.push_back(firstCase);
140+
141+
std::string secondCase = relocatedModulePath;
142+
secondCase += moduleName.str() + '/';
143+
secondCase += moduleNameWithExt.str() + '/';
144+
moduleLocations.push_back(secondCase);
145+
146+
std::string thirdCase = relocatedModulePath;
147+
thirdCase += moduleName.str() + '/';
148+
thirdCase += moduleName.str() + ".framework/Modules/";
149+
thirdCase += moduleNameWithExt.str() + '/';
150+
moduleLocations.push_back(thirdCase);
151+
152+
std::vector<std::string> aliases;
153+
for (auto& location : moduleLocations) {
154+
aliases.push_back(location + arch + ".swiftmodule");
155+
if (!targetTriple.empty()) {
156+
llvm::Triple triple(targetTriple);
157+
auto moduleTriple = swift::getTargetSpecificModuleTriple(triple);
158+
aliases.push_back(location + moduleTriple.normalize() + ".swiftmodule");
159+
}
160+
}
161+
162+
return aliases;
163+
}
164+
165+
namespace codeql {
166+
167+
std::unordered_map<std::string, std::string> rewriteOutputsInPlace(
168+
SwiftExtractorConfiguration& config,
169+
std::vector<std::string>& CLIArgs) {
170+
std::unordered_map<std::string, std::string> remapping;
171+
172+
// TODO: handle filelists?
173+
const std::unordered_set<std::string> pathRewriteOptions({
174+
"-emit-dependencies-path",
175+
"-emit-module-path",
176+
"-emit-module-doc-path",
177+
"-emit-module-source-info-path",
178+
"-emit-objc-header-path",
179+
"-emit-reference-dependencies-path",
180+
"-index-store-path",
181+
"-module-cache-path",
182+
"-o",
183+
"-pch-output-dir",
184+
"-serialize-diagnostics-path",
185+
});
186+
187+
std::unordered_set<std::string> outputFileMaps(
188+
{"-supplementary-output-file-map", "-output-file-map"});
189+
190+
std::vector<size_t> outputFileMapIndexes;
191+
std::vector<std::string> maybeInput;
192+
std::string targetTriple;
193+
194+
std::vector<std::string> newLocations;
195+
for (size_t i = 0; i < CLIArgs.size(); i++) {
196+
if (pathRewriteOptions.count(CLIArgs[i])) {
197+
auto oldPath = CLIArgs[i + 1];
198+
auto newPath = config.tempArtifactDir + '/' + oldPath;
199+
CLIArgs[++i] = newPath;
200+
newLocations.push_back(newPath);
201+
202+
remapping[oldPath] = newPath;
203+
} else if (outputFileMaps.count(CLIArgs[i])) {
204+
// collect output map indexes for further rewriting and skip the following argument
205+
// We don't patch the map in place as we need to collect all the input files first
206+
outputFileMapIndexes.push_back(++i);
207+
} else if (CLIArgs[i] == "-target") {
208+
targetTriple = CLIArgs[++i];
209+
} else if (CLIArgs[i][0] != '-') {
210+
// TODO: add support for input file lists?
211+
// We need to collect input file names to later use them to extract information from the
212+
// output file maps.
213+
maybeInput.push_back(CLIArgs[i]);
214+
}
215+
}
216+
217+
for (auto index : outputFileMapIndexes) {
218+
auto oldPath = CLIArgs[index];
219+
auto maybeNewPath = rewriteOutputFileMap(config, oldPath, maybeInput, remapping);
220+
if (maybeNewPath) {
221+
auto newPath = maybeNewPath.value();
222+
CLIArgs[index] = newPath;
223+
remapping[oldPath] = newPath;
224+
}
225+
}
226+
227+
// This doesn't really belong here, but we've got Xcode...
228+
for (const auto& [oldPath, newPath] : remapping) {
229+
llvm::StringRef path(oldPath);
230+
auto aliases = computeModuleAliases(path, targetTriple);
231+
for (auto& alias : aliases) {
232+
remapping[alias] = newPath;
233+
}
234+
}
235+
236+
return remapping;
237+
}
238+
239+
void ensureDirectoriesForNewPathsExist(
240+
const std::unordered_map<std::string, std::string>& remapping) {
241+
for (auto& [_, newPath] : remapping) {
242+
llvm::SmallString<PATH_MAX> filepath(newPath);
243+
llvm::StringRef parent = llvm::sys::path::parent_path(filepath);
244+
if (std::error_code ec = llvm::sys::fs::create_directories(parent)) {
245+
std::cerr << "Cannot create redirected directory: " << ec.message() << "\n";
246+
}
247+
}
248+
}
249+
250+
void storeRemappingForVFS(const SwiftExtractorConfiguration& config,
251+
const std::unordered_map<std::string, std::string>& remapping) {
252+
// Only create remapping for the .swiftmodule files
253+
std::unordered_map<std::string, std::string> modules;
254+
for (const auto& [oldPath, newPath] : remapping) {
255+
if (llvm::StringRef(oldPath).endswith(".swiftmodule")) {
256+
modules.emplace(oldPath, newPath);
257+
}
258+
}
259+
260+
if (modules.empty()) {
261+
return;
262+
}
263+
264+
if (std::error_code ec = llvm::sys::fs::create_directories(config.tempVFSDir)) {
265+
std::cerr << "Cannot create temp VFS directory: " << ec.message() << "\n";
266+
return;
267+
}
268+
269+
if (std::error_code ec = llvm::sys::fs::create_directories(config.VFSDir)) {
270+
std::cerr << "Cannot create VFS directory: " << ec.message() << "\n";
271+
return;
272+
}
273+
274+
// Constructing the VFS yaml file in a temp folder so that the other process doesn't read it
275+
// while it is not complete
276+
// TODO: Pick a more robust way to not collide with files from other processes
277+
auto tempVfsPath = config.tempVFSDir + '/' + std::to_string(getpid()) + "-vfs.yaml";
278+
std::error_code ec;
279+
llvm::raw_fd_ostream fd(tempVfsPath, ec, llvm::sys::fs::OF_None);
280+
if (ec) {
281+
std::cerr << "Cannot create temp VFS file: '" << tempVfsPath << "': " << ec.message() << "\n";
282+
return;
283+
}
284+
// TODO: there must be a better API than this
285+
// LLVM expects the version to be 0
286+
fd << "{ version: 0,\n";
287+
// This tells the FS not to fallback to the physical file system in case the remapped file is not
288+
// present
289+
fd << " fallthrough: false,\n";
290+
fd << " roots: [\n";
291+
for (auto& [oldPath, newPath] : modules) {
292+
fd << " {\n";
293+
fd << " type: 'file',\n";
294+
fd << " name: '" << oldPath << "\',\n";
295+
fd << " external-contents: '" << newPath << "\'\n";
296+
fd << " },\n";
297+
}
298+
fd << " ]\n";
299+
fd << "}\n";
300+
301+
fd.flush();
302+
auto vfsPath = config.VFSDir + '/' + std::to_string(getpid()) + "-vfs.yaml";
303+
if (std::error_code ec = llvm::sys::fs::rename(tempVfsPath, vfsPath)) {
304+
std::cerr << "Cannot move temp VFS file '" << tempVfsPath << "' -> '" << vfsPath
305+
<< "': " << ec.message() << "\n";
306+
return;
307+
}
308+
}
309+
310+
std::vector<std::string> collectVFSFiles(const SwiftExtractorConfiguration& config) {
311+
auto vfsDir = config.VFSDir + '/';
312+
if (!llvm::sys::fs::exists(vfsDir)) {
313+
return {};
314+
}
315+
std::vector<std::string> overlays;
316+
std::error_code ec;
317+
llvm::sys::fs::directory_iterator it(vfsDir, ec);
318+
while (!ec && it != llvm::sys::fs::directory_iterator()) {
319+
llvm::StringRef path(it->path());
320+
if (path.endswith("vfs.yaml")) {
321+
overlays.push_back(path.str());
322+
}
323+
it.increment(ec);
324+
}
325+
326+
return overlays;
327+
}
328+
329+
} // namespace codeql

0 commit comments

Comments
 (0)