blob: 26c27428292f6b5ccefe95a9ba5e2390ef92f4fe [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed 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.
//
// wrapped_clang.cc: Pass args to 'xcrun clang' and optionally produce dSYM
// files.
//
#include <libgen.h>
#include <spawn.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <array>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <utility>
#include <vector>
extern char **environ;
namespace {
constexpr char kAddASTPathPrefix[] = "-Wl,-add_ast_path,";
// Returns the base name of the given filepath. For example, given
// /foo/bar/baz.txt, returns 'baz.txt'.
const char *Basename(const char *filepath) {
const char *base = strrchr(filepath, '/');
return base ? (base + 1) : filepath;
}
// Unescape and unquote an argument read from a line of a response file.
static std::string Unescape(const std::string &arg) {
std::string result;
auto length = arg.size();
for (size_t i = 0; i < length; ++i) {
auto ch = arg[i];
// If it's a backslash, consume it and append the character that follows.
if (ch == '\\' && i + 1 < length) {
++i;
result.push_back(arg[i]);
continue;
}
// If it's a quote, process everything up to the matching quote, unescaping
// backslashed characters as needed.
if (ch == '"' || ch == '\'') {
auto quote = ch;
++i;
while (i != length && arg[i] != quote) {
if (arg[i] == '\\' && i + 1 < length) {
++i;
}
result.push_back(arg[i]);
++i;
}
if (i == length) {
break;
}
continue;
}
// It's a regular character.
result.push_back(ch);
}
return result;
}
// Converts an array of string arguments to char *arguments.
// The first arg is reduced to its basename as per execve conventions.
// Note that the lifetime of the char* arguments in the returned array
// are controlled by the lifetime of the strings in args.
std::vector<const char *> ConvertToCArgs(const std::vector<std::string> &args) {
std::vector<const char *> c_args;
c_args.push_back(Basename(args[0].c_str()));
for (int i = 1; i < args.size(); i++) {
c_args.push_back(args[i].c_str());
}
c_args.push_back(nullptr);
return c_args;
}
// Spawns a subprocess for given arguments args. The first argument is used
// for the executable path.
bool RunSubProcess(const std::vector<std::string> &args) {
std::vector<const char *> exec_argv = ConvertToCArgs(args);
pid_t pid;
int status = posix_spawn(&pid, args[0].c_str(), nullptr, nullptr,
const_cast<char **>(exec_argv.data()), environ);
if (status == 0) {
int wait_status;
do {
wait_status = waitpid(pid, &status, 0);
} while ((wait_status == -1) && (errno == EINTR));
if (wait_status < 0) {
std::cerr << "Error waiting on child process '" << args[0] << "'. "
<< strerror(errno) << "\n";
return false;
}
if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
std::cerr << "Error in child process '" << args[0] << "'. "
<< WEXITSTATUS(status) << "\n";
return false;
} else if (WIFSIGNALED(status)) {
std::cerr << "Error in child process '" << args[0] << "'. "
<< WTERMSIG(status) << "\n";
return false;
}
} else {
std::cerr << "Error forking process '" << args[0] << "'. "
<< strerror(status) << "\n";
return false;
}
return true;
}
// Finds and replaces all instances of oldsub with newsub, in-place on str.
void FindAndReplace(const std::string &oldsub, const std::string &newsub,
std::string *str) {
int start = 0;
while ((start = str->find(oldsub, start)) != std::string::npos) {
str->replace(start, oldsub.length(), newsub);
start += newsub.length();
}
}
// If arg is of the classic flag form "foo=bar", and flagname is 'foo', sets
// str to point to a new std::string 'bar' and returns true.
// Otherwise, returns false.
bool SetArgIfFlagPresent(const std::string &arg, const std::string &flagname,
std::string *str) {
std::string prefix_string = flagname + "=";
if (arg.compare(0, prefix_string.length(), prefix_string) == 0) {
*str = arg.substr(prefix_string.length());
return true;
}
return false;
}
// Returns the DEVELOPER_DIR environment variable in the current process
// environment. Aborts if this variable is unset.
std::string GetMandatoryEnvVar(const std::string &var_name) {
char *env_value = getenv(var_name.c_str());
if (env_value == nullptr) {
std::cerr << "Error: " << var_name << " not set.\n";
exit(EXIT_FAILURE);
}
return env_value;
}
// Returns true if `str` starts with the specified `prefix`.
bool StartsWith(const std::string &str, const std::string &prefix) {
return str.compare(0, prefix.size(), prefix) == 0;
}
// If *`str` begins `prefix`, strip it out and return true.
// Otherwise leave *`str` unchanged and return false.
bool StripPrefixStringIfPresent(std::string *str, const std::string &prefix) {
if (StartsWith(*str, prefix)) {
*str = str->substr(prefix.size());
return true;
}
return false;
}
// An RAII temporary file.
class TempFile {
public:
// Create a new temporary file using the given path template string (the same
// form used by `mkstemp`). The file will automatically be deleted when the
// object goes out of scope.
static std::unique_ptr<TempFile> Create(const std::string &path_template) {
const char *tmpDir = getenv("TMPDIR");
if (!tmpDir) {
tmpDir = "/tmp";
}
size_t size = strlen(tmpDir) + path_template.size() + 2;
std::unique_ptr<char[]> path(new char[size]);
snprintf(path.get(), size, "%s/%s", tmpDir, path_template.c_str());
if (mkstemp(path.get()) == -1) {
std::cerr << "Failed to create temporary file '" << path.get()
<< "': " << strerror(errno) << "\n";
return nullptr;
}
return std::unique_ptr<TempFile>(new TempFile(path.get()));
}
// Explicitly make TempFile non-copyable and movable.
TempFile(const TempFile &) = delete;
TempFile &operator=(const TempFile &) = delete;
TempFile(TempFile &&) = default;
TempFile &operator=(TempFile &&) = default;
~TempFile() { remove(path_.c_str()); }
// Gets the path to the temporary file.
std::string GetPath() const { return path_; }
private:
explicit TempFile(const std::string &path) : path_(path) {}
std::string path_;
};
static std::unique_ptr<TempFile> WriteResponseFile(
const std::vector<std::string> &args) {
auto response_file = TempFile::Create("wrapped_clang_params.XXXXXX");
std::ofstream response_file_stream(response_file->GetPath());
for (const auto &arg : args) {
// When Clang/Swift write out a response file to communicate from driver to
// frontend, they just quote every argument to be safe; we duplicate that
// instead of trying to be "smarter" and only quoting when necessary.
response_file_stream << '"';
for (auto ch : arg) {
if (ch == '"' || ch == '\\') {
response_file_stream << '\\';
}
response_file_stream << ch;
}
response_file_stream << "\"\n";
}
response_file_stream.close();
return response_file;
}
void ProcessArgument(const std::string arg, const std::string developer_dir,
const std::string sdk_root, const std::string cwd,
bool relative_ast_path, std::string toolchain_path,
std::function<void(const std::string &)> consumer);
bool ProcessResponseFile(const std::string arg, const std::string developer_dir,
const std::string sdk_root, const std::string cwd,
bool relative_ast_path, std::string toolchain_path,
std::function<void(const std::string &)> consumer) {
auto path = arg.substr(1);
std::ifstream original_file(path);
// Ignore non-file args such as '@loader_path/...'
if (!original_file.good()) {
return false;
}
std::string arg_from_file;
while (std::getline(original_file, arg_from_file)) {
// Arguments in response files might be quoted/escaped, so we need to
// unescape them ourselves.
ProcessArgument(Unescape(arg_from_file), developer_dir, sdk_root, cwd,
relative_ast_path, toolchain_path, consumer);
}
return true;
}
std::string GetCurrentDirectory() {
// Passing null,0 causes getcwd to allocate the buffer of the correct size.
char *buffer = getcwd(nullptr, 0);
std::string cwd(buffer);
free(buffer);
return cwd;
}
std::string exec(std::string cmd) {
std::array<char, 128> buffer;
std::string result;
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"),
pclose);
if (!pipe) {
std::cerr << "Error: failed to open pipe to '" << cmd << "'" << std::endl;
exit(EXIT_FAILURE);
}
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
result += buffer.data();
}
return result;
}
std::string GetToolchainPath(const std::string &toolchain_id) {
// NOTE: This requires all toolchains to contain a 'clang' executable. This
// is true today for custom Swift toolchains, but could change in the future.
std::string output = exec("xcrun --find clang --toolchain " + toolchain_id);
if (output.empty()) {
std::cerr << "Error: TOOLCHAINS was set to '" << toolchain_id
<< "' but no toolchain with that ID was found" << std::endl;
exit(EXIT_FAILURE);
} else if (output.find("XcodeDefault.xctoolchain") != std::string::npos) {
// NOTE: Ideally xcrun would fail if the toolchain we asked for didn't exist
// but it falls back to the DEVELOPER_DIR instead, so we have to check the
// output ourselves.
std::cerr << "Error: TOOLCHAINS was set to '" << toolchain_id
<< "' but the default toolchain was found, that likely means a "
"matching "
<< "toolchain isn't installed" << std::endl;
exit(EXIT_FAILURE);
}
std::filesystem::path toolchain_path(output);
// Remove usr/bin/clang components to get the root of the custom toolchain
return toolchain_path.parent_path().parent_path().parent_path();
}
void ProcessArgument(const std::string arg, const std::string developer_dir,
const std::string sdk_root, const std::string cwd,
bool relative_ast_path, std::string toolchain_path,
std::function<void(const std::string &)> consumer) {
auto new_arg = arg;
if (arg[0] == '@') {
if (ProcessResponseFile(arg, developer_dir, sdk_root, cwd,
relative_ast_path, toolchain_path, consumer)) {
return;
}
}
FindAndReplace("__BAZEL_EXECUTION_ROOT__", cwd, &new_arg);
FindAndReplace("__BAZEL_XCODE_DEVELOPER_DIR__", developer_dir, &new_arg);
FindAndReplace("__BAZEL_XCODE_SDKROOT__", sdk_root, &new_arg);
if (!toolchain_path.empty()) {
FindAndReplace("__BAZEL_CUSTOM_XCODE_TOOLCHAIN_PATH__", toolchain_path,
&new_arg);
}
// Make the `add_ast_path` options used to embed Swift module references
// absolute to enable Swift debugging without dSYMs: see
// https://forums.swift.org/t/improving-swift-lldb-support-for-path-remappings/22694
if (!relative_ast_path &&
StripPrefixStringIfPresent(&new_arg, kAddASTPathPrefix)) {
// Only modify relative paths.
if (!StartsWith(arg, "/")) {
new_arg = std::string(kAddASTPathPrefix) + cwd + "/" + new_arg;
} else {
new_arg = std::string(kAddASTPathPrefix) + new_arg;
}
}
consumer(new_arg);
}
void AddLayeringCheckVFS(const std::string vfs_overlay_file,
const char *modulemap, const std::string developer_dir,
std::function<void(const std::string &)> consumer) {
std::ofstream vfs_overlay_file_stream(vfs_overlay_file);
vfs_overlay_file_stream
<< R"EOF({"case-sensitive":true,"overlay-relative":false,"roots":[{"contents":[{"external-contents":")EOF"
<< modulemap
<< R"EOF(","name":"vfs.modulemap","type":"file"}],"name":")EOF"
<< developer_dir
<< R"EOF(","type":"directory"}],"use-external-names":false,"version":0})EOF"
<< std::endl;
consumer("-ivfsoverlay" + vfs_overlay_file);
consumer("-Xclang");
consumer("-fmodule-map-file=" + developer_dir + "/vfs.modulemap");
}
} // namespace
int main(int argc, char *argv[]) {
std::string tool_name;
std::string binary_name = Basename(argv[0]);
if (binary_name == "wrapped_clang_pp") {
tool_name = "clang++";
} else if (binary_name == "wrapped_clang") {
tool_name = "clang";
} else {
std::cerr << "Binary must either be named 'wrapped_clang' or "
"'wrapped_clang_pp', not "
<< binary_name << "\n";
return 1;
}
const char *toolchain_id = getenv("TOOLCHAINS");
std::string toolchain_path = "";
if (toolchain_id != nullptr) {
toolchain_path = GetToolchainPath(toolchain_id);
}
std::string developer_dir = GetMandatoryEnvVar("DEVELOPER_DIR");
std::string sdk_root = GetMandatoryEnvVar("SDKROOT");
const std::string cwd = GetCurrentDirectory();
std::vector<std::string> invocation_args = {"/usr/bin/xcrun", tool_name};
std::vector<std::string> processed_args = {};
bool relative_ast_path = getenv("RELATIVE_AST_PATH") != nullptr;
auto consumer = [&](const std::string &arg) {
processed_args.push_back(arg);
};
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
ProcessArgument(arg, developer_dir, sdk_root, cwd, relative_ast_path,
toolchain_path, consumer);
}
char *modulemap = getenv("APPLE_SUPPORT_MODULEMAP");
std::unique_ptr<TempFile> vfs_overlay_file;
if (modulemap != nullptr) {
vfs_overlay_file = TempFile::Create("modules-vfs-overlay.XXXXXX");
AddLayeringCheckVFS(vfs_overlay_file->GetPath(), modulemap, developer_dir,
consumer);
}
// Special mode that only prints the command. Used for testing.
if (getenv("__WRAPPED_CLANG_LOG_ONLY")) {
for (const std::string &arg : invocation_args) std::cout << arg << ' ';
for (const std::string &arg : processed_args) std::cout << arg << ' ';
std::cout << "\n";
return 0;
}
auto response_file = WriteResponseFile(processed_args);
invocation_args.push_back("@" + response_file->GetPath());
// Check to see if we should postprocess with dsymutil.
bool postprocess = false;
const char *linked_binary = getenv("DSYM_HINT_LINKED_BINARY");
const char *dsym_path_raw_value = getenv("DSYM_HINT_DSYM_PATH");
if (linked_binary != nullptr || dsym_path_raw_value != nullptr) {
if (linked_binary == nullptr || dsym_path_raw_value == nullptr) {
const char *missing_dsym_flag;
if (linked_binary == nullptr) {
missing_dsym_flag = "DSYM_HINT_LINKED_BINARY";
} else {
missing_dsym_flag = "DSYM_HINT_DSYM_PATH";
}
std::cerr << "Error in clang wrapper: If any dsym "
"hint is defined, then "
<< missing_dsym_flag << " must be defined\n";
return 1;
} else {
postprocess = true;
}
}
if (!RunSubProcess(invocation_args)) {
return 1;
}
const char *header_parsing_output = getenv("HEADER_PARSING_OUTPUT");
if (header_parsing_output != nullptr) {
std::ofstream output(header_parsing_output);
output.close();
}
if (!postprocess) {
return 0;
}
std::string dsym_path = dsym_path_raw_value;
const std::string bundle_suffix = ".dSYM";
bool is_bundle = dsym_path.rfind(bundle_suffix) ==
dsym_path.length() - bundle_suffix.length();
std::vector<std::string> dsymutil_args = {
"/usr/bin/xcrun", "dsymutil",
linked_binary, "-o",
dsym_path, "--no-swiftmodule-timestamp"};
if (!is_bundle) {
// We should generate a .dSYM bundle only when a path is passed to a .dSYM
// directory for backwards compatibility
dsymutil_args.push_back("--flat");
}
if (!RunSubProcess(dsymutil_args)) {
return 1;
}
// When stripping is requested, we should still strip the binary
// before returning
const char *strip_debug_symbols = getenv("STRIP_DEBUG_SYMBOLS");
if (strip_debug_symbols != nullptr) {
std::vector<std::string> strip_args = {"/usr/bin/xcrun", "strip", "-S",
linked_binary};
if (!RunSubProcess(strip_args)) {
return 2;
}
}
return 0;
}