2021-06-20 09:54:45 -03:00
/*
* 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 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 ;
}
2021-11-10 20:55:02 -03:00
static Result < Vector < String > , int > run_whiptail ( WhiptailMode mode , Vector < WhiptailOption > const & options , StringView title , StringView description )
2021-06-20 09:54:45 -03:00
{
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 \n checkbox=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 ) ;
}
2021-11-10 20:55:02 -03:00
static bool run_system_command ( String const & command , StringView command_name )
2021-06-20 09:54:45 -03:00
{
2022-07-11 14:32:29 -03:00
if ( command . starts_with ( " cmake " sv ) )
2021-06-20 09:54:45 -03:00
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 ;
}
2021-11-01 12:11:36 -03:00
auto current_working_directory = Core : : File : : current_working_directory ( ) ;
if ( current_working_directory . is_null ( ) )
2021-06-20 09:54:45 -03:00
return 1 ;
2021-11-01 12:11:36 -03:00
auto lexical_cwd = LexicalPath ( current_working_directory ) ;
2021-06-20 09:54:45 -03:00
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.
2022-02-06 10:33:42 -03:00
auto components_file = Core : : ConfigFile : : open ( " components.ini " ) . release_value_but_fixme_should_propagate_errors ( ) ;
2021-06-20 09:54:45 -03:00
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 } ) ;
2022-07-11 14:32:29 -03:00
auto configs_result = run_whiptail ( WhiptailMode : : Menu , configs , " SerenityOS - System Configurations " sv , " Which system configuration do you want to use or customize? " sv ) ;
2021-06-20 09:54:45 -03:00
if ( configs_result . is_error ( ) ) {
warnln ( " ConfigureComponents cancelled. " ) ;
return 0 ;
}
VERIFY ( configs_result . value ( ) . size ( ) = = 1 ) ;
auto type = configs_result . value ( ) . first ( ) ;
2022-07-11 14:32:29 -03:00
bool customize = type . starts_with ( " CUSTOM_ " sv ) ;
2021-06-20 09:54:45 -03:00
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 ( ' ' ) ;
2022-07-11 14:32:29 -03:00
description_builder . append ( " [required] " sv ) ;
2021-06-20 09:54:45 -03:00
}
// 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 ) ) ;
}
2022-07-11 14:32:29 -03:00
auto result = run_whiptail ( WhiptailMode : : Checklist , options , " SerenityOS - System Components " sv , " Which optional system components do you want to include? " sv ) ;
2021-06-20 09:54:45 -03:00
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 ) ;
2022-07-11 14:32:29 -03:00
if ( ! run_system_command ( command , " CMake " sv ) )
2021-06-20 09:54:45 -03:00
return 1 ;
2022-07-11 14:32:29 -03:00
if ( ! run_system_command ( " ninja clean " sv , " Ninja " sv ) )
2021-06-20 09:54:45 -03:00
return 1 ;
2022-07-11 14:32:29 -03:00
if ( ! run_system_command ( " rm -rf Root " sv , " rm " sv ) )
2021-06-20 09:54:45 -03:00
return 1 ;
return 0 ;
}