blob: 3ff718ee242ab89818c0ab8a2d69484429f2d1d9 [file] [log] [blame]
// Copyright 2025 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.
#include <spawn.h>
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <unordered_set>
extern char **environ;
const std::regex libRegex = std::regex(".*\\.a$");
const std::regex singleArgFlags = std::regex("-arch_only|-syslibroot|-o");
// An RAII temporary directory.
class TempDirectory {
public:
// Create a new temporary directory. The directory will automatically be
// deleted when the object goes out of scope.
static std::unique_ptr<TempDirectory> Create() {
std::filesystem::path temp_path = std::filesystem::temp_directory_path() /
"bazel_libtool_symlinks.XXXXXXX";
std::unique_ptr<char[]> path(new char[temp_path.string().size() + 1]);
std::strcpy(path.get(), temp_path.string().c_str());
char *temp_dir = mkdtemp(path.get());
if (temp_dir == nullptr) {
std::cerr << "error: failed to create temporary directory" << std::endl;
exit(EXIT_FAILURE);
}
return std::unique_ptr<TempDirectory>(new TempDirectory(path.get()));
}
// Explicitly make TempDirectory non-copyable and movable.
TempDirectory(const TempDirectory &) = delete;
TempDirectory &operator=(const TempDirectory &) = delete;
TempDirectory(TempDirectory &&) = default;
TempDirectory &operator=(TempDirectory &&) = default;
~TempDirectory() { std::filesystem::remove_all(path_.c_str()); }
std::filesystem::path GetPath() const { return path_; }
private:
explicit TempDirectory(const std::string &path) : path_(path) {}
std::string path_;
};
// 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 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;
}
// 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) {
int pipefd[2]; // File descriptors for the pipe
if (pipe(pipefd) == -1) {
perror("pipe failed");
return false;
}
std::vector<const char *> exec_argv = ConvertToCArgs(args);
pid_t pid;
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
// Redirect child's stdout to the write end of the pipe
posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDERR_FILENO);
posix_spawn_file_actions_addclose(&actions,
pipefd[0]); // Close unused read end
int status = posix_spawn(&pid, args[0].c_str(), &actions, nullptr,
const_cast<char **>(exec_argv.data()), environ);
close(pipefd[1]); // Close write end in the parent
if (status == 0) {
posix_spawn_file_actions_destroy(&actions);
int wait_status;
char buffer[256];
// Drain stderr pipe first, once closed we'll check waitpid
ssize_t count;
std::ostringstream oss;
while ((count = read(pipefd[0], buffer, sizeof(buffer) - 1)) > 0) {
oss.write(buffer, count);
}
do {
wait_status = waitpid(pid, &status, 0);
} while ((wait_status == -1) && (errno == EINTR));
std::stringstream ss(oss.str());
std::string line;
while (std::getline(ss, line)) {
if (line.find("(no object file members in the library define global "
"symbols)") == std::string::npos) {
std::cerr << line << "\n";
}
}
close(pipefd[0]); // Close read end
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();
}
}
std::string rewriteArg(const std::string arg, const std::string developer_dir,
const std::string sdk_root) {
auto new_arg = arg;
findAndReplace("__BAZEL_XCODE_DEVELOPER_DIR__", developer_dir, &new_arg);
findAndReplace("__BAZEL_XCODE_SDKROOT__", sdk_root, &new_arg);
return new_arg;
}
bool hasDuplicateBasenames(const std::vector<std::string> files) {
std::unordered_set<std::string> basenames;
for (std::filesystem::path path : files) {
const auto pair = basenames.insert(path.filename());
if (!pair.second) {
return true;
}
}
return false;
}
void processArgs(const std::vector<std::string> args,
std::function<void(const std::string &)> flags_consumer,
std::function<void(const std::string &)> files_consumer) {
for (auto it = args.begin(); it != args.end(); ++it) {
const std::string arg = *it;
if (arg == "-filelist") {
++it;
std::ifstream list(*it);
for (std::string line; std::getline(list, line);) {
files_consumer(line);
}
} else if (arg[0] == '@') {
std::string paramsFilePath(arg.substr(1));
std::ifstream params_file(paramsFilePath);
std::vector<std::string> params_file_args = {};
for (std::string line; std::getline(params_file, line);) {
params_file_args.push_back(line);
}
processArgs(params_file_args, flags_consumer, files_consumer);
} else if (regex_match(arg, singleArgFlags)) {
flags_consumer(arg);
++it;
flags_consumer(*it);
} else if (regex_match(arg, libRegex)) {
flags_consumer(arg);
} else if (arg[0] == '-') { // Assume any dashed flag is valid, otherwise
// it will fail in real libtool anyways
flags_consumer(arg);
} else { // Assume all other args are object files
files_consumer(arg);
}
}
}
std::string hash(const std::string &input) {
std::hash<std::string> hasher;
size_t hashValue = hasher(input);
std::ostringstream oss;
oss << std::hex << std::setw(sizeof(size_t) * 2) << std::setfill('0')
<< hashValue;
return oss.str();
}
void createSymlinks(std::filesystem::path temp_directory,
const std::vector<std::string> files,
std::function<void(const std::string &)> files_consumer) {
for (auto file : files) {
std::filesystem::path path = file;
std::hash<std::string> hasher;
hasher(file);
std::string new_basename = path.stem();
new_basename.append("_");
new_basename.append(hash(file));
new_basename.append(".o");
auto link = temp_directory / new_basename;
std::filesystem::create_symlink(std::filesystem::absolute(path), link);
files_consumer(link);
}
}
int main(int argc, const char *argv[]) {
std::vector<std::string> args;
// Set i to 1 to skip executable path
for (int i = 1; argv[i] != nullptr; i++) {
args.push_back(argv[i]);
}
std::string developer_dir = getMandatoryEnvVar("DEVELOPER_DIR");
std::string sdk_root = getMandatoryEnvVar("SDKROOT");
// NOTE: Order of libtool flags interspersed with files does not matter, but
// maintaining file order might?
std::vector<std::string> processed_args = {};
std::vector<std::string> files = {};
auto flags_consumer = [&](const std::string &arg) {
processed_args.push_back(rewriteArg(arg, developer_dir, sdk_root));
};
auto files_consumer = [&](const std::string &arg) {
files.push_back(rewriteArg(arg, developer_dir, sdk_root));
};
processArgs(args, flags_consumer, files_consumer);
std::unique_ptr<TempDirectory> temp_directory = TempDirectory::Create();
if (hasDuplicateBasenames(files)) {
createSymlinks(temp_directory->GetPath(), files, flags_consumer);
} else {
processed_args.insert(processed_args.end(), files.begin(), files.end());
}
auto response_file = temp_directory->GetPath() / "bazel_libtool.params";
std::ofstream response_file_stream(response_file);
for (const auto &arg : processed_args) {
response_file_stream << '"';
for (auto ch : arg) {
if (ch == '"') {
response_file_stream << '\\';
}
response_file_stream << ch;
}
response_file_stream << "\"\n";
}
response_file_stream.close();
std::vector<std::string> invocation_args = {
"/usr/bin/xcrun",
"libtool",
"@" + response_file.u8string(),
};
if (!runSubProcess(invocation_args)) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}