Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Environment Variable Support #83

Merged
merged 9 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ jobs:
id: build-orc-orc_dogfood
continue-on-error: true
run: |
xcodebuild -project ./build/orc.xcodeproj -scheme orc_dogfood -configuration Debug
ORC_OUTPUT_FILE=./output.json ORC_OUTPUT_FILE_MODE=json xcodebuild -json -project ./build/orc.xcodeproj -scheme orc_dogfood -configuration Debug -hideShellScriptEnvironment
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new test that leverages the environment variables to cause a JSON file to get dumped when running orc_dogfood (which is a CMake target that uses ORC to build ORC). The JSON is then traversed with a couple jq queries to make sure some boilerplate keys are in there and filled in with something meaningful.

cat output.json
echo # newline
if [ `cat output.json | jq '.["synopsis"]["violations"]'` == 0 ]; then echo "SUCCESS"; else echo "FAILURE"; exit 1; fi
if [ `cat output.json | jq '.["synopsis"]["dies_skipped"]'` != 0 ]; then echo "SUCCESS"; else echo "FAILURE"; exit 1; fi
if [ `cat output.json | jq '.["synopsis"]["unique_symbols"]'` != 0 ]; then echo "SUCCESS"; else echo "FAILURE"; exit 1; fi
if [ `cat output.json | jq '.["synopsis"]["object_files_scanned"]'` != 0 ]; then echo "SUCCESS"; else echo "FAILURE"; exit 1; fi
- name: 🛠️ orc release
id: build-orc-release
continue-on-error: true
Expand Down
4 changes: 3 additions & 1 deletion include/orc/str.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ std::string format_size(std::size_t x, format_mode mode = format_mode::binary);
std::string format_pct(float x);

inline std::string format_pct(float x, float total) {
return format_pct(x / total);
return format_pct(total ? x / total : 0);
}

std::string toupper(std::string&& s);

/**************************************************************************************************/
225 changes: 132 additions & 93 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,42 @@ void open_output_file(const std::string& a, const std::string& b) {

/**************************************************************************************************/

void process_orc_config_file(const char* bin_path_string) {
template <typename T>
T parse_enval(std::string&&) = delete;

template <>
std::string parse_enval(std::string&& x) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why move semantics instead of const std::string&?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. the value coming from getenv is a const char*, which is then converted to a std::string for this routine. For the std::string specialization of parse_enval, we can move the incoming string to the result with no additional changes. If the incoming string were const std::string&, it would cost us a copy to make a new one to return by value.

assert(!x.empty());
return x;
}

template <>
bool parse_enval(std::string&& x) {
assert(!x.empty());
return x != "0" && toupper(std::move(x)) != "FALSE";
}

template <>
std::size_t parse_enval(std::string&& x) {
assert(!x.empty());
return std::max<int>(std::atoi(x.c_str()), 0);
}

template <typename T>
T derive_configuration(const char* key,
const toml::parse_result& settings,
T&& fallback) {
T result = settings[key].value_or(fallback);
std::string envar = toupper(std::string("ORC_") + key);
if (const char* enval = std::getenv(envar.c_str())) {
result = parse_enval<T>(enval);
}
return result;
}
Comment on lines +108 to +118
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This routine is the heart of the PR. Instead of taking values from the settings file directly, derive_configuration will also look at the associated environment variable and use that if found.


/**************************************************************************************************/

void process_orc_configuration(const char* bin_path_string) {
std::filesystem::path pwd(std::filesystem::current_path());
std::filesystem::path bin_path(pwd / bin_path_string);
std::filesystem::path config_path;
Expand Down Expand Up @@ -114,104 +149,106 @@ void process_orc_config_file(const char* bin_path_string) {
bin_path = parent;
}

toml::parse_result settings;

if (exists(config_path)) {
try {
auto settings = toml::parse_file(config_path.string());
auto& app_settings = settings::instance();

app_settings._graceful_exit = settings["graceful_exit"].value_or(false);
app_settings._max_violation_count = settings["max_error_count"].value_or(0);
app_settings._forward_to_linker = settings["forward_to_linker"].value_or(true);
app_settings._standalone_mode = settings["standalone_mode"].value_or(false);
app_settings._dylib_scan_mode = settings["dylib_scan_mode"].value_or(false);
app_settings._parallel_processing = settings["parallel_processing"].value_or(true);
app_settings._filter_redundant = settings["filter_redundant"].value_or(true);
app_settings._print_object_file_list = settings["print_object_file_list"].value_or(false);
app_settings._relative_output_file = settings["relative_output_file"].value_or("");

if (settings["output_file"]) {
std::string fn = *settings["output_file"].value<std::string>();
open_output_file("", fn);
}
settings = toml::parse_file(config_path.string());
} catch (const toml::parse_error& err) {
cerr_safe([&](auto& s) { s << "Parsing failed:\n" << err << "\n"; });
throw std::runtime_error("configuration parsing error");
}
} else {
cerr_safe([&](auto& s) { s << "ORC config file: not found\n"; });
}

if (auto log_level = settings["log_level"].value<std::string>()) {
const auto& level = *log_level;
if (level == "silent") {
app_settings._log_level = settings::log_level::silent;
} else if (level == "warning") {
app_settings._log_level = settings::log_level::warning;
} else if (level == "info") {
app_settings._log_level = settings::log_level::info;
} else if (level == "verbose") {
app_settings._log_level = settings::log_level::verbose;
} else {
// not a known value. Switch to verbose!
app_settings._log_level = settings::log_level::verbose;
cout_safe(
[&](auto& s) { s << "warning: Unknown log level (using verbose)\n"; });
}
}
auto& app_settings = settings::instance();

app_settings._graceful_exit = derive_configuration("graceful_exit", settings, false);
app_settings._max_violation_count = derive_configuration("max_error_count", settings, std::size_t(0));
app_settings._forward_to_linker = derive_configuration("forward_to_linker", settings, true);
app_settings._standalone_mode = derive_configuration("standalone_mode", settings, false);
app_settings._dylib_scan_mode = derive_configuration("dylib_scan_mode", settings, false);
app_settings._parallel_processing = derive_configuration("parallel_processing", settings, true);
app_settings._filter_redundant = derive_configuration("filter_redundant", settings, true);
app_settings._print_object_file_list = derive_configuration("print_object_file_list", settings, false);
app_settings._relative_output_file = derive_configuration("relative_output_file", settings, std::string());

const std::string log_level = derive_configuration("log_level", settings, std::string("warning"));
const std::string output_file = derive_configuration("output_file", settings, std::string());
const std::string output_file_mode = derive_configuration("output_file_mode", settings, std::string("text"));

// Do this early so we can log the ensuing output if it happens.
if (!output_file.empty()) {
open_output_file(std::string(), output_file);
}

if (app_settings._standalone_mode && app_settings._dylib_scan_mode) {
throw std::logic_error(
"Both standalone and dylib scanning mode are enabled. Pick one.");
}
// Do this early, too, so we can _suppress_ the output if we need to.
if (output_file_mode == "text") {
app_settings._output_file_mode = settings::output_file_mode::text;
} else if (output_file_mode == "json") {
app_settings._output_file_mode = settings::output_file_mode::json;
} else if (log_level_at_least(settings::log_level::warning)) {
cout_safe([&](auto& s) {
s << "warning: unknown output_file_mode '" << output_file_mode << "'; using text\n";
});
}

if (app_settings._dylib_scan_mode) {
app_settings._forward_to_linker = false;
}
if (log_level == "silent") {
app_settings._log_level = settings::log_level::silent;
} else if (log_level == "warning") {
app_settings._log_level = settings::log_level::warning;
} else if (log_level == "info") {
app_settings._log_level = settings::log_level::info;
} else if (log_level == "verbose") {
app_settings._log_level = settings::log_level::verbose;
} else {
// not a known value. Switch to verbose!
app_settings._log_level = settings::log_level::verbose;
cout_safe(
[&](auto& s) { s << "warning: unknown log_level '" << log_level << "'; using verbose\n"; });
}

auto read_string_list = [&_settings = settings](const char* name) {
std::vector<std::string> result;
if (auto* array = _settings.get_as<toml::array>(name)) {
for (const auto& entry : *array) {
if (auto symbol = entry.value<std::string>()) {
result.push_back(*symbol);
}
}
}
std::sort(result.begin(), result.end());
return result;
};

app_settings._symbol_ignore = read_string_list("symbol_ignore");
app_settings._violation_report = read_string_list("violation_report");
app_settings._violation_ignore = read_string_list("violation_ignore");

if (!app_settings._violation_report.empty() &&
!app_settings._violation_ignore.empty()) {
if (log_level_at_least(settings::log_level::warning)) {
cout_safe([&](auto& s) {
s << "warning: Both `violation_report` and `violation_ignore` lists found\n";
s << "warning: `violation_report` will be ignored in favor of `violation_ignore`\n";
});
}
}
if (app_settings._standalone_mode && app_settings._dylib_scan_mode) {
throw std::logic_error(
"Both standalone and dylib scanning mode are enabled. Pick one.");
}

if (app_settings._dylib_scan_mode) {
app_settings._forward_to_linker = false;
}

if (settings["output_file_mode"]) {
const std::string mode = *settings["output_file_mode"].value<std::string>();
if (mode == "text") {
app_settings._output_file_mode = settings::output_file_mode::text;
} else if (mode == "json") {
app_settings._output_file_mode = settings::output_file_mode::json;
} else if (log_level_at_least(settings::log_level::warning)) {
cout_safe([&](auto& s) {
s << "warning: unknown `output_file_mode`: " << mode << "\n";
});
auto read_string_list = [&_settings = settings](const char* name) {
std::vector<std::string> result;
if (auto* array = _settings.get_as<toml::array>(name)) {
for (const auto& entry : *array) {
if (auto symbol = entry.value<std::string>()) {
result.push_back(*symbol);
}
}
}
std::sort(result.begin(), result.end());
return result;
};

if (log_level_at_least(settings::log_level::info)) {
cout_safe([&](auto& s) {
s << "info: ORC config file: " << config_path.string() << "\n";
});
}
} catch (const toml::parse_error& err) {
cerr_safe([&](auto& s) { s << "Parsing failed:\n" << err << "\n"; });
throw std::runtime_error("configuration parsing error");
app_settings._symbol_ignore = read_string_list("symbol_ignore");
app_settings._violation_report = read_string_list("violation_report");
app_settings._violation_ignore = read_string_list("violation_ignore");

if (!app_settings._violation_report.empty() &&
!app_settings._violation_ignore.empty()) {
if (log_level_at_least(settings::log_level::warning)) {
cout_safe([&](auto& s) {
s << "warning: Both `violation_report` and `violation_ignore` lists found\n";
s << "warning: `violation_report` will be ignored in favor of `violation_ignore`\n";
});
}
} else {
cerr_safe([&](auto& s) { s << "ORC config file: not found\n"; });
}

if (log_level_at_least(settings::log_level::info)) {
cout_safe([&](auto& s) {
s << "info: ORC config file: " << config_path.string() << "\n";
});
}
}

Expand Down Expand Up @@ -407,11 +444,13 @@ auto epilogue(bool exception) {
});
}

if (exception || g._odrv_count != 0) {
return settings::instance()._graceful_exit ? EXIT_SUCCESS : EXIT_FAILURE;
if (exception) {
return EXIT_FAILURE;
} else if (settings::instance()._graceful_exit) {
return EXIT_SUCCESS;
}

return EXIT_SUCCESS;
return g._odrv_count == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

/**************************************************************************************************/
Expand Down Expand Up @@ -483,7 +522,7 @@ int main(int argc, char** argv) try {

signal(SIGINT, interrupt_callback_handler);

process_orc_config_file(argv[0]);
process_orc_configuration(argv[0]);

cmdline_results cmdline = process_command_line(argc, argv);

Expand Down Expand Up @@ -547,7 +586,7 @@ int main(int argc, char** argv) try {
return epilogue(true);
} catch (...) {
cerr_safe([&](auto& s) { s << "Fatal error: unknown\n"; });
return epilogue(false);
return epilogue(true);
}

/**************************************************************************************************/
6 changes: 3 additions & 3 deletions src/orc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ void to_json(nlohmann::json& j, const odrv_report::conflict_details& c) {
for (std::size_t i = 0; i < ancestry._count; ++i) {
const std::string key = ancestry._ancestors[i].allocate_string();
if (i == (ancestry._count - 1)) {
node->push_back(key);
(*node)["object_files"].push_back(key);
} else {
node = &(*node)[key];
}
Expand Down Expand Up @@ -644,7 +644,7 @@ std::string to_json(const std::vector<odrv_report>& reports) {
constexpr auto spaces_k = 0; // no pretty-printing in release builds
#endif

nlohmann::json violations;
nlohmann::json violations = nlohmann::json::object_t();

for (const auto& report : reports) {
assert(violations.count(report._symbol) == 0);
Expand All @@ -657,7 +657,7 @@ std::string to_json(const std::vector<odrv_report>& reports) {
synopsis["object_files_scanned"] = g._object_file_count.load();
synopsis["dies_processed"] = g._die_processed_count.load();
synopsis["dies_skipped"] = g._die_skipped_count.load();
synopsis["dies_skipped_pct"] = g._die_skipped_count * 100. / g._die_processed_count;
synopsis["dies_skipped_pct"] = g._die_processed_count ? (g._die_skipped_count * 100. / g._die_processed_count) : 0;
synopsis["unique_symbols"] = g._unique_symbol_count.load();

nlohmann::json result = nlohmann::json::object_t {
Expand Down
9 changes: 9 additions & 0 deletions src/str.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,12 @@ std::string format_pct(float x) {
}

/**************************************************************************************************/

std::string toupper(std::string&& s) {
std::transform(s.begin(), s.end(), s.begin(), [](auto c){
return std::toupper(c);
});
return s;
}

/**************************************************************************************************/
Loading