blob: ee66904a5ee1d64dc3d198400d9966a6b37a7520 [file] [log] [blame]
// Part of the Crubit project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "rs_bindings_from_cc/cmdline.h"
#include <algorithm>
#include <fstream>
#include <iterator>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "absl/debugging/leak_check.h"
#include "absl/flags/flag.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "common/ffi_types.h"
#include "common/status_macros.h"
#include "rs_bindings_from_cc/bazel_types.h"
#include "rs_bindings_from_cc/cmdline_flags.h"
#include "rs_bindings_from_cc/ir.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/JSON.h"
ABSL_FLAG(bool, do_nothing, false,
"if set to true the tool will produce empty files "
"(useful for testing Bazel integration)");
ABSL_FLAG(std::string, rs_out, "",
"output path for the Rust source file with bindings");
ABSL_FLAG(std::string, cc_out, "",
"output path for the C++ source file with bindings implementation");
ABSL_FLAG(std::string, ir_out, "",
"(optional) output path for the JSON IR. If not present, the JSON IR "
"will not be dumped.");
ABSL_FLAG(std::string, crubit_support_path_format, "",
"the format of `#include` for including Crubit C++ support library "
"headers in the "
"generated .cc files, in the format specifier, use `{header}` as the "
"placeholder. For "
"example, to include `support_header.h` as "
"`#include <crubit/support/support_header.h>, specify "
"`<crubit/support/{header}>`; for "
"`#include \"crubit/support/support_header.h\", specify "
"`\"crubit/support/{header}`,");
ABSL_FLAG(std::string, clang_format_exe_path, "",
"Path to a clang-format executable that will be used to format the "
".cc files generated by the tool.");
ABSL_FLAG(std::string, rustfmt_exe_path, "",
"Path to a rustfmt executable that will be used to format the "
".rs files generated by the tool.");
ABSL_FLAG(std::string, rustfmt_config_path, "",
"(optional) path to a rustfmt.toml file that should replace the "
"default formatting of the .rs files generated by the tool.");
ABSL_FLAG(std::vector<std::string>, public_headers, std::vector<std::string>(),
"public headers of the cc_library this tool should generate bindings "
"for, in a format suitable for usage in google3-relative quote "
"include (#include \"\").");
ABSL_FLAG(std::string, target, "", "The target to generate bindings for.");
ABSL_FLAG(std::string, target_args, "",
"Per-target Crubit arguments, encoded as a JSON array. This contains "
"both the list of headers assigned to the target (h), and the set of "
"enabled features (f). For example:"
"[\n"
" {\n"
" \"t\": \"//foo/bar:baz\",\n"
" \"h\": [\"foo/bar/header1.h\", \"foo/bar/header2.h\"],\n"
" \"f\": [\"supported\"]\n"
" },\n"
"...\n"
"]");
ABSL_FLAG(std::vector<std::string>, extra_rs_srcs, std::vector<std::string>(),
"Additional Rust source files to include into the crate.");
ABSL_FLAG(std::vector<std::string>, srcs_to_scan_for_instantiations,
std::vector<std::string>(),
"[template instantiation mode only] all Rust source files of a crate "
"for which we are instantiating templates.");
ABSL_FLAG(std::string, instantiations_out, "",
"[template instantiation mode only] output path for the JSON file "
"with mapping from a template instantiation to a generated Rust "
"struct name. This file is used by cc_template! macro expansion.");
ABSL_FLAG(std::string, namespaces_out, "",
"(optional) output path for the JSON file containing the target's"
"namespace hierarchy.");
ABSL_FLAG(std::string, error_report_out, "",
"(optional) output path for the JSON error report");
ABSL_FLAG(bool, generate_source_location_in_doc_comment, true,
"add the source code location from which the binding originates in"
"the doc comment of the binding");
namespace crubit {
namespace {
struct TargetArgs {
std::string target;
std::vector<std::string> headers;
std::vector<std::string> features;
};
bool fromJSON(const llvm::json::Value& json, TargetArgs& out,
llvm::json::Path path) {
llvm::json::ObjectMapper mapper(json, path);
return mapper && mapper.map("t", out.target) &&
mapper.mapOptional("h", out.headers) &&
mapper.mapOptional("f", out.features);
}
std::vector<HeaderName> PublicHeaders() {
std::vector<HeaderName> public_headers;
const std::vector<std::string>& public_headers_string =
absl::GetFlag(FLAGS_public_headers);
std::transform(public_headers_string.begin(), public_headers_string.end(),
std::back_inserter(public_headers),
[](const std::string& s) { return HeaderName(s); });
return public_headers;
}
} // namespace
namespace internal {
absl::Status ParseTargetArgs(absl::string_view target_args_str,
CmdlineArgs& args) {
if (target_args_str.empty()) {
return absl::InvalidArgumentError("please specify --target_args");
}
auto target_args =
llvm::json::parse<std::vector<TargetArgs>>(std::move(target_args_str));
if (auto err = target_args.takeError()) {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed `--target_args` argument: ", toString(std::move(err))));
}
for (const TargetArgs& it : *target_args) {
const std::string& target = it.target;
if (target.empty()) {
return absl::InvalidArgumentError(
"Expected `t` fields of `--target_args` to be a non-empty "
"string");
}
for (const std::string& header : it.headers) {
if (header.empty()) {
return absl::InvalidArgumentError(
"Expected `h` (header) fields of `--target_args` to be an "
"array of non-empty strings");
}
BazelLabel target_label(target);
auto [it, inserted] = args.headers_to_targets.try_emplace(
HeaderName(header), std::move(target_label));
if (!inserted) {
LOG(WARNING) << "The `--target_args` cmdline argument assigns `"
<< header << "` header to two conflicting targets: `"
<< target << "` vs `" << it->second.value() << "`";
// Assign the one that comes first alphabetically, to get a consistent
// result.
if (target_label.value() < it->second.value()) {
it->second = std::move(target_label);
}
}
}
for (const std::string& feature : it.features) {
if (feature.empty()) {
return absl::InvalidArgumentError(
"Expected `f` (feature) fields of `--target_args` to be an "
"array of non-empty strings");
}
args.target_to_features[BazelLabel(target)].insert(feature);
}
}
return absl::OkStatus();
}
} // namespace internal
absl::StatusOr<Cmdline> Cmdline::FromFlags() {
auto args = CmdlineArgs{
.current_target = BazelLabel(absl::GetFlag(FLAGS_target)),
.cc_out = absl::GetFlag(FLAGS_cc_out),
.rs_out = absl::GetFlag(FLAGS_rs_out),
.ir_out = absl::GetFlag(FLAGS_ir_out),
.namespaces_out = absl::GetFlag(FLAGS_namespaces_out),
.crubit_support_path_format =
absl::GetFlag(FLAGS_crubit_support_path_format),
.clang_format_exe_path = absl::GetFlag(FLAGS_clang_format_exe_path),
.rustfmt_exe_path = absl::GetFlag(FLAGS_rustfmt_exe_path),
.rustfmt_config_path = absl::GetFlag(FLAGS_rustfmt_config_path),
.error_report_out = absl::GetFlag(FLAGS_error_report_out),
.do_nothing = absl::GetFlag(FLAGS_do_nothing),
.generate_source_location_in_doc_comment =
absl::GetFlag(FLAGS_generate_source_location_in_doc_comment)
? SourceLocationDocComment::Enabled
: SourceLocationDocComment::Disabled,
.public_headers = PublicHeaders(),
.extra_rs_srcs = absl::GetFlag(FLAGS_extra_rs_srcs),
.srcs_to_scan_for_instantiations =
absl::GetFlag(FLAGS_srcs_to_scan_for_instantiations),
.instantiations_out = absl::GetFlag(FLAGS_instantiations_out)};
absl::Status parse_target_args_status =
internal::ParseTargetArgs(absl::GetFlag(FLAGS_target_args), args);
absl::StatusOr<Cmdline> cmdline = Cmdline::Create(std::move(args));
if (!parse_target_args_status.ok() || !cmdline.ok()) {
return absl::InvalidArgumentError(
absl::StrCat(cmdline.status().message(), cmdline.ok() ? "" : "\n",
parse_target_args_status.message()));
}
return cmdline;
}
absl::StatusOr<Cmdline> Cmdline::Create(CmdlineArgs args) {
std::string error;
if (args.current_target.empty()) {
absl::StrAppend(&error, "please specify --target\n");
}
if (args.rs_out.empty()) {
absl::StrAppend(&error, "please specify --rs_out\n");
}
if (args.cc_out.empty()) {
absl::StrAppend(&error, "please specify --cc_out\n");
}
if (args.public_headers.empty()) {
absl::StrAppend(&error, "please specify --public_headers\n");
}
if (args.clang_format_exe_path.empty()) {
absl::StrAppend(&error, "please specify --clang_format_exe_path\n");
}
if (args.rustfmt_exe_path.empty()) {
absl::StrAppend(&error, "please specify --rustfmt_exe_path\n");
}
if (args.crubit_support_path_format.empty()) {
absl::StrAppend(&error, "please specify --crubit_support_path_format\n");
} else if (!absl::StrContains(args.crubit_support_path_format, "{header}")) {
absl::StrAppend(
&error,
"cannot find `{header}` placeholder in crubit_support_path_format\n");
}
if (args.srcs_to_scan_for_instantiations.empty() !=
args.instantiations_out.empty()) {
absl::StrAppend(
&error,
"please specify both --rust_sources and --instantiations_out when "
"requesting a template instantiation mode\n");
}
for (const HeaderName& header : args.public_headers) {
if (auto it = args.headers_to_targets.find(header);
it == args.headers_to_targets.end()) {
absl::StrAppend(
&error,
absl::Substitute(
"Couldn't find header '$0' in the `headers_to_target` map "
"derived from the --target_args cmdline argument\n",
header.IncludePath()));
}
}
if (!error.empty()) {
error.erase(error.size() - 1); // remove trailing \n
return absl::InvalidArgumentError(error);
}
return Cmdline(std::move(args));
}
void ExpandParamfiles(int& argc, char**& argv) {
std::vector<char*> new_argv; // Will be leaked if we find a paramfile.
char** begin = argv;
char** end = begin + argc;
for (;;) {
char** next_paramfile =
std::find_if(begin, end, [](char* c) { return c[0] == '@'; });
if (next_paramfile == end) break;
new_argv.insert(new_argv.end(), begin, next_paramfile);
begin = next_paramfile + 1;
const char* paramfile = *next_paramfile + 1;
std::ifstream in(paramfile);
std::stringstream ss;
ss << in.rdbuf();
std::string s = ss.str();
std::string next_arg;
// Unfortunately, we can't just use something like StrReplaceAll, because an
// escaped newline should be part of the value, while an unescaped newline
// should not be.
for (auto it = s.begin(); it != s.end(); ++it) {
if (*it == '\\') {
++it;
if (it == s.end()) {
absl::StrAppend(&next_arg, "\\");
break;
} else {
absl::StrAppend(&next_arg, absl::string_view(&*it, 1));
}
} else if (*it == '\n') {
new_argv.push_back(
absl::IgnoreLeak(new auto(std::move(next_arg)))->data());
next_arg.clear();
} else {
absl::StrAppend(&next_arg, absl::string_view(&*it, 1));
}
}
if (!next_arg.empty()) {
new_argv.push_back(
absl::IgnoreLeak(new auto(std::move(next_arg)))->data());
}
}
if (begin == argv) return;
new_argv.insert(new_argv.end(), begin, end);
argv = new_argv.data();
argc = new_argv.size();
absl::IgnoreLeak(new auto(std::move(new_argv))); // nowhere else to put it.
}
void PreprocessTargetArgs(int& argc, char** argv) {
// TODO(jeanpierreda): Now that flag parsing logic is no longer in a bash script,
// this should probably skip target_args entirely. (For now, the target_args
// flag is left in place for compatibility and to avoid test churn.) We put it
// in the place of the first --target_to_arg flag, because we can't put it at
// the end, because of `--`.
char** end = argv + argc;
static constexpr absl::string_view kTargetToArg = "--target_to_arg";
char** first_target_to_arg = std::find(argv, end, kTargetToArg);
if (first_target_to_arg == end) {
return;
}
std::string& target_args =
*absl::IgnoreLeak(new std::string("--target_args=["));
bool is_target_arg = true;
bool is_done = false;
auto new_end = std::remove_if(first_target_to_arg + 1, end, [&](char* arg) {
if (is_done) {
return false;
}
if (is_target_arg) {
absl::StrAppend(&target_args, arg, ",");
is_target_arg = false;
return true;
} else if (arg == kTargetToArg) {
is_target_arg = true;
return true;
} else if (arg == absl::string_view("--")) {
is_done = true;
return false;
} else {
return false;
}
});
target_args.pop_back(); // remove trailing comma.
absl::StrAppend(&target_args, "]");
*first_target_to_arg = target_args.data();
argc = new_end - argv;
}
} // namespace crubit