From 2d71eaadcdbdc97d1acb33d3aecab36171b8aa9d Mon Sep 17 00:00:00 2001 From: Max Wipfli Date: Sun, 20 Jun 2021 14:54:45 +0200 Subject: [PATCH] Meta: Add the ConfigureComponents utility This adds a utility program which is essentially a command generator for CMake. It reads the 'components.ini' file generated by CMake in the build directory, prompts the user to select a build type and optionally customize it, generates and runs a CMake command as well as 'ninja clean' and 'rm -rf Root', which are needed to properly remove system components. The program uses whiptail(1) for user interaction. --- CMakeLists.txt | 7 + Meta/CMake/ConfigureComponents/CMakeLists.txt | 6 + Meta/CMake/ConfigureComponents/main.cpp | 374 ++++++++++++++++++ 3 files changed, 387 insertions(+) create mode 100644 Meta/CMake/ConfigureComponents/CMakeLists.txt create mode 100644 Meta/CMake/ConfigureComponents/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 992292e470c..a6c22fe3c17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,12 @@ add_custom_target(install-ports USES_TERMINAL ) +add_custom_target(configure-components + COMMAND ConfigureComponents + DEPENDS ConfigureComponents + USES_TERMINAL +) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -131,6 +137,7 @@ endif() add_subdirectory(Userland/DevTools/IPCCompiler) add_subdirectory(Userland/DevTools/StateMachineGenerator) add_subdirectory(Userland/Libraries/LibWeb/CodeGenerators) +add_subdirectory(Meta/CMake/ConfigureComponents) set(write_if_different ${CMAKE_SOURCE_DIR}/Meta/write-only-on-difference.sh) diff --git a/Meta/CMake/ConfigureComponents/CMakeLists.txt b/Meta/CMake/ConfigureComponents/CMakeLists.txt new file mode 100644 index 00000000000..acc15f9dde5 --- /dev/null +++ b/Meta/CMake/ConfigureComponents/CMakeLists.txt @@ -0,0 +1,6 @@ +set(SOURCES + main.cpp +) + +add_executable(ConfigureComponents ${SOURCES}) +target_link_libraries(ConfigureComponents LagomCore) diff --git a/Meta/CMake/ConfigureComponents/main.cpp b/Meta/CMake/ConfigureComponents/main.cpp new file mode 100644 index 00000000000..7f6949416db --- /dev/null +++ b/Meta/CMake/ConfigureComponents/main.cpp @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2021, Max Wipfli + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class ComponentCategory { + Optional, + Recommended, + Required +}; + +struct ComponentData { + String name; + String description; + ComponentCategory category { ComponentCategory::Optional }; + bool was_selected { false }; + Vector dependencies; + bool is_selected { false }; +}; + +struct WhiptailOption { + String tag; + String name; + String description; + bool checked { false }; +}; + +enum class WhiptailMode { + Menu, + Checklist +}; + +static Optional get_current_working_directory() +{ + char* cwd = getcwd(nullptr, 0); + if (!cwd) { + perror("getcwd"); + return {}; + } + String data { cwd }; + free(cwd); + return data; +} + +static Vector read_component_data(Core::ConfigFile const& config_file) +{ + VERIFY(!config_file.read_entry("Global", "build_everything", {}).is_empty()); + Vector components; + + auto groups = config_file.groups(); + quick_sort(groups, [](auto& a, auto& b) { + return a.to_lowercase() < b.to_lowercase(); + }); + + for (auto& component_name : groups) { + if (component_name == "Global") + continue; + auto description = config_file.read_entry(component_name, "description", ""); + auto recommended = config_file.read_bool_entry(component_name, "recommended", false); + auto required = config_file.read_bool_entry(component_name, "required", false); + auto user_selected = config_file.read_bool_entry(component_name, "user_selected", false); + auto depends = config_file.read_entry(component_name, "depends", "").split(';'); + // NOTE: Recommended and required shouldn't be set at the same time. + VERIFY(!recommended || !required); + ComponentCategory category { ComponentCategory::Optional }; + if (recommended) + category = ComponentCategory::Recommended; + else if (required) + category = ComponentCategory ::Required; + + components.append(ComponentData { component_name, move(description), category, user_selected, move(depends), false }); + } + + return components; +} + +static Result, int> run_whiptail(WhiptailMode mode, Vector const& options, StringView const& title, StringView const& description) +{ + struct winsize w; + if (ioctl(0, TIOCGWINSZ, &w) < 0) { + perror("ioctl"); + return -errno; + } + + auto height = w.ws_row - 6; + auto width = min(w.ws_col - 6, 80); + + int pipefd[2]; + if (pipe(pipefd) < 0) { + perror("pipefd"); + return -errno; + } + + int read_fd = pipefd[0]; + int write_fd = pipefd[1]; + + Vector arguments = { "whiptail", "--notags", "--separate-output", "--output-fd", String::number(write_fd) }; + + if (!title.is_empty()) { + arguments.append("--title"); + arguments.append(title); + } + + switch (mode) { + case WhiptailMode::Menu: + arguments.append("--menu"); + break; + case WhiptailMode::Checklist: + arguments.append("--checklist"); + break; + default: + VERIFY_NOT_REACHED(); + } + + if (description.is_empty()) + arguments.append(String::empty()); + else + arguments.append(String::formatted("\n {}", description)); + + arguments.append(String::number(height)); + arguments.append(String::number(width)); + arguments.append(String::number(height - 9)); + + // Check how wide the name field needs to be. + size_t max_name_width = 0; + for (auto& option : options) { + if (option.name.length() > max_name_width) + max_name_width = option.name.length(); + } + + for (auto& option : options) { + arguments.append(option.tag); + arguments.append(String::formatted("{:{2}} {}", option.name, option.description, max_name_width)); + if (mode == WhiptailMode::Checklist) + arguments.append(option.checked ? "1" : "0"); + } + + char* argv[arguments.size() + 1]; + for (size_t i = 0; i < arguments.size(); ++i) + argv[i] = const_cast(arguments[i].characters()); + argv[arguments.size()] = nullptr; + + auto* term_variable = getenv("TERM"); + if (!term_variable) { + warnln("getenv: TERM variable not set."); + close(write_fd); + close(read_fd); + return -1; + } + + auto full_term_variable = String::formatted("TERM={}", term_variable); + auto colors = "NEWT_COLORS=root=,black\ncheckbox=black,lightgray"; + + char* env[3]; + env[0] = const_cast(full_term_variable.characters()); + env[1] = const_cast(colors); + env[2] = nullptr; + + pid_t pid; + if (posix_spawnp(&pid, arguments[0].characters(), nullptr, nullptr, argv, env)) { + perror("posix_spawnp"); + warnln("\e[31mError:\e[0m Could not execute 'whiptail', maybe it isn't installed."); + close(write_fd); + close(read_fd); + return -errno; + } + + int status = -1; + if (waitpid(pid, &status, 0) < 0) { + perror("waitpid"); + close(write_fd); + close(read_fd); + return -errno; + } + + close(write_fd); + + if (!WIFEXITED(status)) { + close(read_fd); + return -1; + } + + int return_code = WEXITSTATUS(status); + if (return_code > 0) { + close(read_fd); + // posix_spawn returns 127 if it cannot exec the child, so maybe 'whiptail' is missing. + if (return_code == 127) + warnln("\e[31mError:\e[0m Could not execute 'whiptail', maybe it isn't installed."); + return return_code; + } + + auto file = Core::File::construct(); + file->open(read_fd, Core::OpenMode::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes); + auto data = String::copy(file->read_all()); + return data.split('\n', false); +} + +static bool run_system_command(String const& command, StringView const& command_name) +{ + if (command.starts_with("cmake")) + warnln("\e[34mRunning CMake...\e[0m"); + else + warnln("\e[34mRunning '{}'...\e[0m", command); + auto rc = system(command.characters()); + if (rc < 0) { + perror("system"); + warnln("\e[31mError:\e[0m Could not run {}.", command_name); + return false; + } else if (rc > 0) { + warnln("\e[31mError:\e[0m {} returned status code {}.", command_name, rc); + return false; + } + return true; +} + +int main() +{ + // Step 1: Check if everything is in order. + if (!isatty(STDIN_FILENO)) { + warnln("Not a terminal!"); + return 1; + } + + auto current_working_directory = get_current_working_directory(); + if (!current_working_directory.has_value()) + return 1; + auto lexical_cwd = LexicalPath(*current_working_directory); + auto& parts = lexical_cwd.parts_view(); + if (parts.size() < 2 || parts[parts.size() - 2] != "Build") { + warnln("\e[31mError:\e[0m This program needs to be executed from inside 'Build/*'."); + return 1; + } + + if (!Core::File::exists("components.ini")) { + warnln("\e[31mError:\e[0m There is no 'components.ini' in the current working directory."); + warnln(" It can be generated by running CMake with 'cmake ../.. -G Ninja'"); + return 1; + } + + // Step 2: Open and parse the 'components.ini' file. + auto components_file = Core::ConfigFile::open("components.ini"); + if (components_file->groups().is_empty()) { + warnln("\e[31mError:\e[0m The 'components.ini' file is either not a valid ini file or contains no entries."); + return 1; + } + + bool build_everything = components_file->read_bool_entry("Global", "build_everything", false); + auto components = read_component_data(components_file); + warnln("{} components were read from 'components.ini'.", components.size()); + + // Step 3: Ask the user which starting configuration to use. + Vector configs; + configs.append({ "REQUIRED", "Required", "Only the essentials.", false }); + configs.append({ "RECOMMENDED", "Recommended", "A sensible collection of programs.", false }); + configs.append({ "FULL", "Full", "All available programs.", false }); + configs.append({ "CUSTOM_REQUIRED", "Required", "Customizable.", false }); + configs.append({ "CUSTOM_RECOMMENDED", "Recommended", "Customizable.", false }); + configs.append({ "CUSTOM_FULL", "Full", "Customizable.", false }); + configs.append({ "CUSTOM_CURRENT", "Current", "Customize current configuration.", false }); + + auto configs_result = run_whiptail(WhiptailMode::Menu, configs, "SerenityOS - System Configurations", "Which system configuration do you want to use or customize?"); + if (configs_result.is_error()) { + warnln("ConfigureComponents cancelled."); + return 0; + } + + VERIFY(configs_result.value().size() == 1); + auto type = configs_result.value().first(); + + bool customize = type.starts_with("CUSTOM_"); + StringView build_type = customize ? type.substring_view(7) : type.view(); + + // Step 4: Customize the configuration if the user requested to. In any case, set the components component.is_selected value correctly. + Vector activated_components; + + if (customize) { + Vector options; + for (auto& component : components) { + auto is_required = component.category == ComponentCategory::Required; + + StringBuilder description_builder; + description_builder.append(component.description); + if (is_required) { + if (!description_builder.is_empty()) + description_builder.append(' '); + description_builder.append("[required]"); + } + + // NOTE: Required components will always be preselected. + WhiptailOption option { component.name, component.name, description_builder.to_string(), is_required }; + if (build_type == "REQUIRED") { + // noop + } else if (build_type == "RECOMMENDED") { + if (component.category == ComponentCategory::Recommended) + option.checked = true; + } else if (build_type == "FULL") { + option.checked = true; + } else if (build_type == "CURRENT") { + if (build_everything || component.was_selected) + option.checked = true; + } else { + VERIFY_NOT_REACHED(); + } + options.append(move(option)); + } + + auto result = run_whiptail(WhiptailMode::Checklist, options, "SerenityOS - System Components", "Which optional system components do you want to include?"); + if (result.is_error()) { + warnln("ConfigureComponents cancelled."); + return 0; + } + + auto selected_components = result.value(); + for (auto& component : components) { + if (selected_components.contains_slow(component.name)) { + component.is_selected = true; + } else if (component.category == ComponentCategory::Required) { + warnln("\e[33mWarning:\e[0m {} was not selected even though it is required. It will be enabled anyway.", component.name); + component.is_selected = true; + } + } + } else { + for (auto& component : components) { + if (build_type == "REQUIRED") + component.is_selected = component.category == ComponentCategory::Required; + else if (build_type == "RECOMMENDED") + component.is_selected = component.category == ComponentCategory::Required || component.category == ComponentCategory::Recommended; + else if (build_type == "FULL") + component.is_selected = true; + else + VERIFY_NOT_REACHED(); + } + } + + // Step 5: Generate the cmake command. + Vector cmake_arguments = { "cmake", "../..", "-G", "Ninja", "-DBUILD_EVERYTHING=OFF" }; + for (auto& component : components) + cmake_arguments.append(String::formatted("-DBUILD_{}={}", component.name.to_uppercase(), component.is_selected ? "ON" : "OFF")); + + warnln("\e[34mThe following command will be run:\e[0m"); + outln("{} \\", String::join(' ', cmake_arguments)); + outln(" && ninja clean\n && rm -rf Root"); + warn("\e[34mDo you want to run the command?\e[0m [Y/n] "); + auto character = getchar(); + if (character == 'n' || character == 'N') { + warnln("ConfigureComponents cancelled."); + return 0; + } + + // Step 6: Run CMake, 'ninja clean' and 'rm -rf Root' + auto command = String::join(' ', cmake_arguments); + if (!run_system_command(command, "CMake")) + return 1; + if (!run_system_command("ninja clean", "Ninja")) + return 1; + if (!run_system_command("rm -rf Root", "rm")) + return 1; + return 0; +}