#!/usr/bin/env perl

# Script to create a configuration file for kdesrc-build.
#
# Copyright © 2011, 2020-2022 Michael Pyne. <mpyne@kde.org>
# Home page: https://apps.kde.org/kdesrc_build/
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

use v5.28;
use strict;
use warnings;

# On many container-based distros, even FindBin is missing to conserve space.
# But we can use File::Spec to do nearly the same.
my $RealBin;
my $modPath;

# The File::Spec calls have to run when parsing (i.e. in BEGIN) to make the
# 'use lib' below work (which itself implicitly uses BEGIN { })
BEGIN {
    use File::Spec;

    # resolve symlinks
    my $scriptPath = $0;
    for (1..16) {
        last unless -l $scriptPath;
        $scriptPath = readlink $scriptPath;
    }
    die "Too many symlinks followed looking for script" if -l $scriptPath;

    my ($volume, $directories, $script) = File::Spec->splitpath($scriptPath);

    $RealBin = File::Spec->catpath($volume, $directories, '');
    die "Couldn't find base directory!" unless $RealBin;

    # Use modules in git repo if running from git dir, otherwise assume
    # system install
    $modPath = File::Spec->rel2abs('modules', $RealBin);
    $modPath = ($RealBin =~ s,/bin/?$,/share/kdesrc-build/modules,r)
        unless -d $modPath;

    die "Couldn't find modules for kdesrc-build-setup!" unless $modPath;
}

use lib "$modPath"; # Make ksb:: modules available

use ksb;
use ksb::FirstRun;

use Cwd qw(abs_path);
use File::Basename;
use File::Copy;
use File::Path qw(make_path);
use File::Temp qw(tempfile);
use IO::Pipe;
use List::Util qw(max min first);

our $VERSION = 0.10; # Not user-visible yet.

my $OS = `uname`; # Check whether we're using Linux or FreeBSD

sub clearScreen
{
    require POSIX;
    my $termios = POSIX::Termios->new();
    $termios->getattr(1); # Get STDOUT attributes

    require Term::Cap;
    my $terminal = Term::Cap->Tgetent({OSPEED => $termios->getospeed});

    # Force the clear characters to be output immediately.
    # Otherwise it might overlap with other output, like error messages.
    local $| = 1;

    print $terminal->Tputs('cl', 0);

    return 0;
}

sub runDialogExecutable (@args)
{
    # Allow for 2 more file descriptors (on top of the normally allowed 0, 1,
    # 2) to survive the upcoming exec
    # See "SYSTEM_FD_MAX" in perldoc:perlvar
    $^F = 4;

    my $pipe = new IO::Pipe;
    my $pid;

    if ($pid = fork()) {
        # Parent
        $pipe->reader();

        my $output = <$pipe>;

        waitpid $pid, 0;
        my $result = ($? >> 8);
        $pipe->close();

        # dialog uses -1 as an exit code, Perl gets just the standard 8 bits
        # the rest of UNIX uses...
        if ($? == -1) {
            clearScreen();
            die "Failed to run dialog(1): $@";
        }
        elsif ($result == 255) {
            clearScreen();
            die "Canceled the dialog";
        }
        return $output || $result;
    }
    elsif (defined $pid) {
        # Child
        $pipe->writer();
        my $outputFd = $pipe->fileno();

        print "Using fd $outputFd";
        exec ('dialog', '--output-fd', $outputFd,
                        '--backtitle', 'kdesrc-build setup',
                        @args);
    }
    else {
        die "Unable to fork? $!";
    }
}

sub getUserInput
{
    my $prompt = shift;
    my $default = shift;

    my @args = qw/--inputbox 8 50/;
    splice @args, 1, 0, $prompt;
    push @args, $default if $default;

    return runDialogExecutable(@args);
}

sub getMenuOption
{
    my ($prompt, @opts) = @_;
    @opts = @{$opts[0]} if ref $opts[0] eq 'ARRAY';

    my @args = qw/--menu 20 70 18/;
    splice @args, 1, 0, $prompt;

    return runDialogExecutable(@args, @opts);
}

sub showInfo
{
    my $message = shift;
    my @args = qw/--msgbox 22 64/;
    splice @args, 1, 0, $message;

    return runDialogExecutable(@args);
}

sub getYesNoAnswer
{
    my $prompt = shift;
    my @args = qw/--yesno 8 55/;
    splice @args, 1, 0, $prompt;

    return runDialogExecutable(@args) == 0;
}

sub getDirectory
{
    my $dir = shift;
    my @args = qw/--dselect 10 70/;
    splice @args, 1, 0, $dir;

    return runDialogExecutable(@args);
}

sub getListOptions
{
    my ($prompt, $opts, $enabled) = @_;
    die "\$opts not a hash ref" unless (ref $opts eq 'ARRAY');
    die "\$enabled not a hash ref" unless (ref $enabled eq 'HASH');

    my @args = qw/--checklist 20 70 18/;
    splice @args, 1, 0, $prompt;
    splice @args, 0, 0, '--output-separator', ',';

    while (my ($k, $v) = splice(@{$opts}, 0, 2)) {
        push (@args, $k, $v, (exists ${$enabled}{$k} ? 'on' : 'off'));
    }

    my $output = runDialogExecutable(@args);

    # Filter out empty results, remove quotes.
    my @items = split (/,/, $output);
    s/^"(.*)"$/$1/ foreach @items;
    @items = grep { length $_ } @items;
    return @items;
}

# The 'dialog(1)' program is required, verify it exists before going
# further.
# We use the --help option since it doesn't send weird terminal characters to the screen
# and it's supported on dialog and Debian's dialog replacement called whiptail.
system('dialog', '--help') == 0 or do {
    my $osError = "$!";

    say "Unable to run the dialog(1) program, it is required for this setup script.";
    if ($? == -1) {
        say "\tThe program wouldn't even run, due to error: $osError";
    }
    else {
        say "\tProgram ran, but exited with error: ", $? >> 8;
    }

    exit 1;
};

showInfo(<<EOF);
This program sets up a base kdesrc-build configuration to
use.

It can be modified as you wish later. Before the form is
presented, you will be asked if you would like an
explanation of the kdesrc-build file layout.  It is
recommended to read this if you are not already familiar
with building software.
EOF

if (getYesNoAnswer('See the tutorial?')) {
    showInfo(<<EOF);
kdesrc-build must download source code from the KDE
repositories.  This source code is then compiled, in the
"build directory". Once complete, this compiled code is
installed to its final location, the "install directory".

This program will only configure the install location, but
all directories are configurable.

The space requirements vary with the amount of software you
choose to build, and whether you keep the build directories
to speed up later builds.  You will probably need at least
20 GiB in total free space unless you take steps to
customize your install to use fewer modules.
EOF
}

# If the user appears to be using a proxy, ask for it directly, otherwise
# prompt for one.
my $proxy = $ENV{http_proxy} // '';

my $installDir = getMenuOption('Where do you want to install the software?',
    [
        home => "$ENV{HOME}/kde/usr (default)",
        custom => "Custom location, chosen next screen",
    ]);

if ($installDir eq 'custom') {
    $installDir = getDirectory('/usr/local/kde');
}
else {
    $installDir = "~/kde/usr";
}

my $sourceDir = getMenuOption('Where do you want the source code to be saved?',
    [
        home => "$ENV{HOME}/kde/src (default)",
        custom => "Custom location, chosen next screen",
    ]);

if ($sourceDir eq 'custom') {
    $sourceDir = getDirectory('/usr/local/kde/src');
}
else {
    $sourceDir = "~/kde/src";
}

my $buildDir = getMenuOption('Where do you want temporary build files to be saved? (They might need lots of space)',
    [
        home => "$ENV{HOME}/kde/build (default)",
        custom => "Custom location, chosen next screen",
    ]);

if ($buildDir eq 'custom') {
    $buildDir = getDirectory('/usr/local/kde/build');
}
else {
    $buildDir = "~/kde/build";
}

showInfo(<<EOF);
Should kdesrc-build automatically try to include needed KDE
dependencies in each build?

Doing this makes it easier to just get a single application
built without worrying about the details. Most KDE software
under active development needs these dependencies.
IF IN ANY DOUBT, SELECT YES on the next screen.

If you know exactly what you want to build or want to ensure
that only modules you have allowed in your configuration
file are built, then you should disable this option.

You can always use the --include-dependencies or
--no-include-dependencies command line option to
kdesrc-build, and you can change this default at any time by
editing the file that this script will generate.
EOF

my $includeDependencies =
    getYesNoAnswer('Should kdesrc-build include KDE dependencies by default with each build?');

my @chosenModules = getListOptions(
    "Which major module groups do you want to build?",
    [
        qt5 => 'Qt 5 - Base support libraries (required if distro version is old)',
        frameworks => 'KDE Frameworks 5 - Essential libraries/runtime (required)',
        workspace => 'KDE Plasma 5 Desktop and workspace',
        base => 'Assorted useful KF5-based applications',
        pim => 'Personal Information Management software',
    ],
    {
        frameworks => 1,
        workspace => 1,
        base => 1,
    },
);

# According to XDG spec, if $XDG_CONFIG_HOME is not set, then we should default
# to ~/.config
my $xdgConfigHome = $ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config";
my $xdgConfigHomeShort = $xdgConfigHome =~ s/^$ENV{HOME}/~/r; # Replace $HOME with ~
my $outputFile = "$xdgConfigHome/kdesrc-buildrc";
my $outputFileShort = $outputFile =~ s/^$ENV{HOME}/~/r; # Replace $HOME with ~
my $output; # Will be output filehandle.

while (-e $outputFile) {
    my $outputChoice = getMenuOption(
        "$outputFileShort already exists, what do you want to do?",
        [
            backup => 'Make a backup, then overwrite with the new configuration',
            custom => 'Write the new configuration to a different file',
            cancel => 'Cancel setup',
        ],
    );

    if ($outputChoice eq 'cancel') {
        showInfo('Setup canceled');
        exit 0;
    }

    if ($outputChoice eq 'custom') {
        $outputFile = getUserInput('Enter desired configuration file name.');
        $outputFile =~ s/^~/$ENV{HOME}/;
        $outputFileShort = $outputFile =~ s/^$ENV{HOME}/~/r;
    }

    if ($outputChoice eq 'backup') {
        copy($outputFile, "$outputFile~") or do {
            my $error = "$!";
            showInfo(<<EOF);
Failed to make backup of $outputFileShort, due to error $error.
Configuration will be written to a temporary file instead.
EOF

            ($output, $outputFile) = tempfile("kdesrc-buildrc-XXXX");
            $outputFileShort = $outputFile =~ s/^$ENV{HOME}/~/r;
        };

        last;
    }
}

# Filehandle could already be opened as a tempfile.
if (!$output) {
    # Ensure the directory we need exists
    my ($vol, $dir, $file) = File::Spec->splitpath($outputFile);
    make_path($dir) unless -d $dir;

    open ($output, '>', $outputFile) or do {
        my $error = "$!";
        showInfo (<<EOF);
Unable to open output file $outputFileShort for writing due to error $error.
EOF
        die "$!";
    }
}

print $output <<EOF;
# Autogenerated by kdesrc-build-setup. You may modify this file if desired.
global
EOF

# Only set qtdir if we're building it ourselves. If user uses their own custom
# Qt they should already be setting PATH and in that case we need do nothing
# anyways.
if (grep /^qt5$/, @chosenModules) {
    print $output <<EOF;

    # The path to your Qt installation (default is empty, assumes Qt provided
    # by system)
    qtdir ~/kde/qt5
EOF
}

my $num_cores;

if ($OS == 'Linux') {
    chomp($num_cores = `nproc`);
} elsif ($OS == 'FreeBSD') {
    chomp($num_cores = `sysctl -n hw.ncpu`);
}
$num_cores ||= 4;

my $num_cores_low = min(ksb::FirstRun::suggestedNumCoresForLowMemory(), $num_cores);

$includeDependencies = $includeDependencies ? 'true' : 'false';

#
### Start generating the kdesrc-buildrc
#
print $output <<EOF;

    # Finds and includes *KDE*-based dependencies into the build.  This makes
    # it easier to ensure that you have all the modules needed, but the
    # dependencies are not very fine-grained so this can result in quite a few
    # modules being installed that you didn't need.
    include-dependencies $includeDependencies

    # Install directory for KDE software
    kdedir $installDir

    # Directory for downloaded source code
    source-dir $sourceDir

    # Directory to build KDE into before installing
    # relative to source-dir by default
    build-dir $buildDir

   ## kdesrc-build sets 2 options which is used in options like make-options or set-env
    # to help manage the number of compile jobs that happen during a build:
    #
    # 1. num-cores, which is just the number of detected CPU cores, and can be passed
    #    to tools like make (needed for parallel build) or ninja (completely optional).
    #
    # 2. num-cores-low-mem, which is set to largest value that appears safe for
    #    particularly heavyweight modules based on total memory, intended for
    #    modules like qtwebengine
    num-cores $num_cores
    num-cores-low-mem $num_cores_low

    # kdesrc-build can install a sample .xsession file for "Custom"
    # (or "XSession") logins,
    install-session-driver false

    # or add a environment variable-setting script to
    # ~/.config/kde-env-master.sh
    install-environment-driver true

    # Stop the build process on the first failure
    stop-on-failure true

    # Use a flat folder layout under $sourceDir and $buildDir
    # rather than nested directories
    directory-layout flat

    # Build with LSP support for everything that supports it
    compile-commands-linking true
    compile-commands-export true
EOF

if ($proxy) {
    print $output <<EOF;

    # Proxy to use for HTTP downloads.
    http-proxy $proxy

    # Prefer HTTPS instead of Git-native protocol for git modules that come
    # from 'kde-projects' repositories.
    #
    # Note that any git:// repositories you use will need to be
    # manually converted to https:// URLs if your network does not allow
    # git:// protcol.
    git-desired-protocol https
EOF
}

# Assume we can refer to files present alongside kdesrc-build in the source
# directory
my $basedir = dirname(abs_path($0));
my $baseDirShort = $basedir =~ s/^$ENV{HOME}/~/r; # Replace $HOME with ~

if (! -e "$basedir/kf5-frameworks-build-include") {
    # Check if it's installed to a share/ prefix
    $basedir = abs_path(dirname($0) . "/../share/kdesrc-build/");
    $baseDirShort = $basedir =~ s/^$ENV{HOME}/~/r;

    if (! -e "$basedir/kf5-frameworks-build-include") {
        close $output;
        showInfo("Unable to find kdesrc-build installation to build a configuration!");
        exit 1;
    }
}

print $output <<EOF;
end global

# Common options that should be set for some KDE modules no matter how
# kdesrc-build finds them. Do not comment these out unless you know
# what you are doing.
include $basedir/kf5-common-options-build-include

EOF

my $do_incl = '#';
$do_incl = '' if grep { $_ eq 'qt5' } @chosenModules;

print $output <<EOF;
# Refers to the qt5 file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
${do_incl}include $basedir/qt5-build-include

# Support libraries that use Qt5
${do_incl}include $basedir/custom-qt5-libs-build-include

EOF

$do_incl = '#';
$do_incl = '' if grep { $_ eq 'frameworks' } @chosenModules;

print $output <<EOF;
# Refers to the kf5-frameworks file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
${do_incl}include $basedir/kf5-frameworks-build-include

EOF

$do_incl = '#';
$do_incl = '' if grep { $_ eq 'workspace' } @chosenModules;

print $output <<EOF;
# Refers to the kf5-workspace file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
${do_incl}include $basedir/kf5-workspace-build-include

EOF

$do_incl = '#';
$do_incl = '' if grep { $_ eq 'base' } @chosenModules;

print $output <<EOF;
# Refers to the kf5-applications file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
${do_incl}include $basedir/kf5-applications-build-include

EOF

$do_incl = '#';
$do_incl = '' if grep { $_ eq 'pim' } @chosenModules;

print $output <<EOF;
# Refers to the kf5-kdepim file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
${do_incl}include $basedir/kf5-kdepim-build-include

EOF

close($output);

showInfo("Generated configuration has been written to $outputFileShort");

if (!@chosenModules) {
    showInfo(<<EOF);
You have not chosen any major module groups. You will have to
add modules and module-sets to your configuration after this.

To help you with this, please consult the sample files in
$basedir
EOF
}

# Say same thing in text mode just in case.
system('clear');
say "Generated configuration has been written to $outputFileShort";
say "Sample configuration files are available in $baseDirShort";

if ($outputFile ne "$xdgConfigHome/kdesrc-buildrc") {
    say <<EOF;

Do note, that your configuration file $outputFileShort will NOT BE USED,
unless you will do one of the following:
- Overwrite $xdgConfigHomeShort/kdesrc-buildrc with $outputFileShort
- Copy $outputFileShort to some directory and rename it to "kdesrc-buildrc",
  then ALWAYS run kdesrc-build from that directory
- ALWAYS pass the "--rc-file $outputFileShort" option to kdesrc-build when
  you run it
EOF
}

exit 0;
