mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-21 23:20:20 +00:00
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.
This commit is contained in:
parent
24c490c520
commit
2d71eaadcd
Notes:
sideshowbarker
2024-07-18 10:16:34 +09:00
Author: https://github.com/MaxWipfli Commit: https://github.com/SerenityOS/serenity/commit/2d71eaadcdb Pull-request: https://github.com/SerenityOS/serenity/pull/8168 Reviewed-by: https://github.com/IdanHo Reviewed-by: https://github.com/gunnarbeutner
3 changed files with 387 additions and 0 deletions
|
@ -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)
|
||||
|
||||
|
|
6
Meta/CMake/ConfigureComponents/CMakeLists.txt
Normal file
6
Meta/CMake/ConfigureComponents/CMakeLists.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
set(SOURCES
|
||||
main.cpp
|
||||
)
|
||||
|
||||
add_executable(ConfigureComponents ${SOURCES})
|
||||
target_link_libraries(ConfigureComponents LagomCore)
|
374
Meta/CMake/ConfigureComponents/main.cpp
Normal file
374
Meta/CMake/ConfigureComponents/main.cpp
Normal file
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Max Wipfli <mail@maxwipfli.ch>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Format.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <AK/Result.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/ConfigFile.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
enum class ComponentCategory {
|
||||
Optional,
|
||||
Recommended,
|
||||
Required
|
||||
};
|
||||
|
||||
struct ComponentData {
|
||||
String name;
|
||||
String description;
|
||||
ComponentCategory category { ComponentCategory::Optional };
|
||||
bool was_selected { false };
|
||||
Vector<String> dependencies;
|
||||
bool is_selected { false };
|
||||
};
|
||||
|
||||
struct WhiptailOption {
|
||||
String tag;
|
||||
String name;
|
||||
String description;
|
||||
bool checked { false };
|
||||
};
|
||||
|
||||
enum class WhiptailMode {
|
||||
Menu,
|
||||
Checklist
|
||||
};
|
||||
|
||||
static Optional<String> get_current_working_directory()
|
||||
{
|
||||
char* cwd = getcwd(nullptr, 0);
|
||||
if (!cwd) {
|
||||
perror("getcwd");
|
||||
return {};
|
||||
}
|
||||
String data { cwd };
|
||||
free(cwd);
|
||||
return data;
|
||||
}
|
||||
|
||||
static Vector<ComponentData> read_component_data(Core::ConfigFile const& config_file)
|
||||
{
|
||||
VERIFY(!config_file.read_entry("Global", "build_everything", {}).is_empty());
|
||||
Vector<ComponentData> 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<Vector<String>, int> run_whiptail(WhiptailMode mode, Vector<WhiptailOption> 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<String> 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<char*>(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<char*>(full_term_variable.characters());
|
||||
env[1] = const_cast<char*>(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<WhiptailOption> 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<String> activated_components;
|
||||
|
||||
if (customize) {
|
||||
Vector<WhiptailOption> 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<String> 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;
|
||||
}
|
Loading…
Reference in a new issue