# SPDX-FileCopyrightText: 2012, 2013, 2016, 2019, 2020, 2022 Michael Pyne <mpyne@kde.org>
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
# SPDX-FileCopyrightText: 2024 Andrew Shark <ashark@linuxcomp.ru>
#
# SPDX-License-Identifier: GPL-2.0-or-later

package ksb::Module 0.20;

use ksb;

=head1 SYNOPSIS

 my $module = ksb::Module->new($ctx, 'module-name');
 $module->update() or die "no update!";
 $module->build()  or die "no build/install!";

 my @modulesFromSet = $moduleSet->convertToModules($ctx);
 say "module name: $_" foreach @modulesFromSet;

=cut

=head1 DESCRIPTION

A subclass of L<ksb::OptionsBase>, which represents a source code module of some
sort that can be updated, built, tested and installed.

This uses abstract interfaces to represent the buildsystem and the source code
version control system (L<ksb::BuildSystem> and L<ksb::Updater> respectively).

Includes a stringifying overload and can be sorted amongst other ksb::Modules.

=cut

use parent qw(ksb::OptionsBase);

use ksb::IPC;
use ksb::Debug;
use ksb::Util qw(:DEFAULT :await);

use ksb::Updater::Git;
use ksb::Updater::KDEProject;
use ksb::Updater::KDEProjectMetadata;
use ksb::Updater::Qt5;

use ksb::BuildException 0.20;

use ksb::BuildSystem 0.30;
use ksb::BuildSystem::Autotools;
use ksb::BuildSystem::QMake;
use ksb::BuildSystem::QMake6;
use ksb::BuildSystem::Qt4;
use ksb::BuildSystem::Qt5;
use ksb::BuildSystem::Qt6;
use ksb::BuildSystem::KDECMake;
use ksb::BuildSystem::CMakeBootstrap;
use ksb::BuildSystem::Meson;

use ksb::ModuleSet::Null;

use Mojo::Util ();
use Mojo::File;

use Storable 'dclone';
use Carp 'confess';
use Scalar::Util 'blessed';
use overload
    '""' => 'toString', # Add stringify operator.
    '<=>' => 'compare',
    ;

sub new
{
    my ($class, $ctx, $name) = @_;

    croak_internal ("Empty ksb::Module constructed") unless $name;

    my $self = ksb::OptionsBase::new($class);

    # If building a BuildContext instead of a ksb::Module, then the context
    # can't have been setup yet...
    my $contextClass = 'ksb::BuildContext';
    if ($class ne $contextClass &&
        (!blessed($ctx) || !$ctx->isa($contextClass)))
    {
        croak_internal ("Invalid context $ctx");
    }

    # Clone the passed-in phases so we can be different. They may be modified later in setOption.
    my $phases = dclone($ctx->phases()) if $ctx;

    my %newOptions = (
        name         => $name,
        scm_obj      => undef,
        build_obj    => undef,
        phases       => $phases,
        context      => $ctx,
        'module-set' => undef,
        post_build_msgs => [ ],
    );

    @{$self}{keys %newOptions} = values %newOptions;

    # Record current values of what would be last source/build dir, if present,
    # before they are potentially reset during the module build.
    $self->setOption('#last-source-dir', $self->getPersistentOption('source-dir') // '');
    $self->setOption('#last-build-dir',  $self->getPersistentOption('build-dir') // '');

    return $self;
}

sub phases
{
    my $self = shift;
    return $self->{phases};
}

sub moduleSet
{
    my ($self) = @_;
    $self->{'module-set'} //= ksb::ModuleSet::Null->new();
    return $self->{'module-set'};
}

sub setModuleSet
{
    my ($self, $moduleSet) = @_;
    assert_isa($moduleSet, 'ksb::ModuleSet');
    $self->{'module-set'} = $moduleSet;
}

# Subroutine to retrieve a subdirectory path with tilde-expansion and
# relative path handling.
# The parameter is the option key (e.g. build-dir or log-dir) to read and
# interpret.
sub getSubdirPath
{
    my ($self, $subdirOption) = @_;
    my $dir = $self->getOption($subdirOption);

    # If build-dir starts with a slash, it is an absolute path.
    return $dir if $dir =~ /^\//;

    # Make sure we got a valid option result.
    if (!$dir) {
        confess ("Reading option for $subdirOption gave empty \$dir!");
    }

    # If it starts with a tilde, expand it out.
    if ($dir =~ /^~/)
    {
        $dir =~ s/^~/$ENV{'HOME'}/;
    }
    else
    {
        # Relative directory, tack it on to the end of $kdesrcdir.
        my $kdesrcdir = $self->getOption('source-dir');
        $dir = "$kdesrcdir/$dir";
    }

    return $dir;
}

# Method: getInstallPathComponents
#
# Returns the directory that a module should be installed in.
#
# NOTE: The return value is a hash. The key 'module' will return the final
# module name, the key 'path' will return the full path to the module. The
# key 'fullpath' will return their concatenation.
#
# For example, with $module == 'KDE/kdelibs', and no change in the dest-dir
# option, you'd get something like:
#
# > {
# >   'path'     => '/home/user/kdesrc/KDE',
# >   'module'   => 'kdelibs',
# >   'fullpath' => '/home/user/kdesrc/KDE/kdelibs'
# > }
#
# If dest-dir were changed to e.g. extragear-multimedia, you'd get:
#
# > {
# >   'path'     => '/home/user/kdesrc',
# >   'module'   => 'extragear-multimedia',
# >   'fullpath' => '/home/user/kdesrc/extragear-multimedia'
# > }
#
# Parameters:
#   pathType - Either 'source' or 'build'.
#
# Returns:
#   hash (Not a hashref; See description).
sub getInstallPathComponents
{
    my $module = assert_isa(shift, 'ksb::Module');
    my $type = shift;
    my $destdir = $module->destDir();
    my $srcbase = $module->getSourceDir();
    $srcbase = $module->getSubdirPath('build-dir') if $type eq 'build';

    my $combined = "$srcbase/$destdir";

    # Remove dup //
    $combined =~ s/\/+/\//;

    my @parts = split(/\//, $combined);
    my %result = ();
    $result{'module'} = pop @parts;
    $result{'path'} = join('/', @parts);
    $result{'fullpath'} = "$result{path}/$result{module}";

    # We used to have code here to migrate very old directory layouts. It was
    # removed as of about 2013-09-29.

    return %result;
}

# Do note that this returns the *base* path to the source directory,
# without the module name or kde_projects stuff appended. If you want that
# use subroutine fullpath().
sub getSourceDir
{
    my $self = shift;
    return $self->getSubdirPath('source-dir');
}

sub name
{
    my $self = shift;
    return $self->{name};
}

sub scm
{
    my $self = shift;

    return $self->{scm_obj} if $self->{scm_obj};

    $self->{scm_obj} //= ksb::Updater::Git->new($self);

    return $self->{scm_obj};
}

sub setScmType
{
    my ($self, $scmType) = @_;

    my $newType;

    if ($scmType eq 'git')          { $newType = ksb::Updater::Git->new($self); }
    elsif ($scmType eq 'proj')     { $newType = ksb::Updater::KDEProject->new($self); }
    elsif ($scmType eq 'metadata') { $newType = ksb::Updater::KDEProjectMetadata->new($self); }
    elsif ($scmType eq 'qt5')      { $newType = ksb::Updater::Qt5->new($self); }
    else                            { $newType = undef; }

    $self->{scm_obj} = $newType;
}

# Returns a string describing the scm platform of the given module.
# Return value: 'git' at this point, as appropriate.
sub scmType
{
    my $self = shift;
    return $self->scm()->name();
}

sub currentScmRevision
{
    my $self = shift;

    return $self->scm()->currentRevisionInternal();
}

# Returns a new build system object, given the appropriate name.
# This is a sub-optimal way to fix the problem of allowing users to override
# the detected build system (we could instead use introspection to figure out
# available build systems at runtime). However, KISS...
sub buildSystemFromName
{
    my ($self, $name) = @_;
    my %buildSystemClasses = (
        'generic'         => 'ksb::BuildSystem',
        'qmake'           => 'ksb::BuildSystem::QMake',
        'qmake6'           => 'ksb::BuildSystem::QMake6',
        'cmake-bootstrap' => 'ksb::BuildSystem::CMakeBootstrap',
        'kde'             => 'ksb::BuildSystem::KDECMake',
        'qt'              => 'ksb::BuildSystem::Qt4',
        'qt5'             => 'ksb::BuildSystem::Qt5',
        'qt6'             => 'ksb::BuildSystem::Qt6',
        'autotools'       => 'ksb::BuildSystem::Autotools',
        'meson'           => 'ksb::BuildSystem::Meson',
    );

    my $class = $buildSystemClasses{lc $name} // undef;
    croak_runtime("Invalid build system $name requested")
        unless $class;

    return $class->new($self);
}

sub buildSystem
{
    my $self = shift;

    if ($self->{build_obj} && $self->{build_obj}->name() ne 'generic') {
        return $self->{build_obj};
    }

    if (my $userBuildSystem = $self->getOption('override-build-system')) {
        $self->{build_obj} = $self->buildSystemFromName($userBuildSystem);
        return $self->{build_obj};
    }

    # If not set, let's guess.
    my $buildType;
    my $sourceDir = $self->fullpath('source');

    # This test must come before the KDE buildsystem's as cmake's own
    # bootstrap system also has CMakeLists.txt
    if (!$buildType && (-e "$sourceDir/CMakeLists.txt") &&
        (-e "$sourceDir/bootstrap"))
    {
        $buildType = ksb::BuildSystem::CMakeBootstrap->new($self);
    }

    if (!$buildType &&
        (-e "$sourceDir/CMakeLists.txt" || $self->isKDEProject()))
    {
        $buildType = ksb::BuildSystem::KDECMake->new($self);
    }

    # We have to assign to an array to force glob to return all results,
    # otherwise it acts like a non-reentrant generator whose output depends on
    # how many times it's been called...
    if (!$buildType && (my @files = glob ("$sourceDir/*.pro"))) {
        $buildType = ksb::BuildSystem::QMake->new($self);
    }

    # 'configure' is a popular fall-back option even for other build
    # systems so ensure we check last for autotools.
    if (!$buildType &&
        (-e "$sourceDir/configure" || -e "$sourceDir/autogen.sh"))
    {
        $buildType = ksb::BuildSystem::Autotools->new($self);
    }

    # Someday move this up, but for now ensure that Meson happens after
    # configure/autotools support is checked for.
    if (!$buildType && -e "$sourceDir/meson.build") {
        $buildType = ksb::BuildSystem::Meson->new($self);
    }

    # Don't just assume the build system is KDE-based...
    $buildType //= ksb::BuildSystem->new($self);

    $self->{build_obj} = $buildType;

    return $self->{build_obj};
}

# Sets the build system **object**, although you can find the build system
# type afterwards (see buildSystemType).
sub setBuildSystem
{
    my ($self, $obj) = @_;

    assert_isa($obj, 'ksb::BuildSystem');
    $self->{build_obj} = $obj;
}

# Current possible build system types:
# KDE (i.e. cmake), Qt, autotools (either
# configure or autogen.sh). A final possibility is 'pendingSource' which
# simply means that we don't know yet.
#
# If the build system type is not set ('pendingSource' counts as being
# set!) when this function is called then it will be autodetected if
# possible, but note that not all possible types will be detected this way.
# If in doubt use setBuildSystemType
sub buildSystemType
{
    my $self = shift;
    return $self->buildSystem()->name();
}

# Subroutine to build this module.
# Returns boolean false on failure, boolean true on success.
sub build
{
    my $self = assert_isa(shift, 'ksb::Module');
    my $moduleName = $self->name();
    my %pathinfo = $self->getInstallPathComponents('build');
    my $builddir = $pathinfo{'fullpath'};
    my $buildSystem = $self->buildSystem();

    if ($buildSystem->name() eq 'generic'
        && !pretending()
        && !$self->hasOption('custom-build-command')
    ) {
        error ("\tr[b[$self] does not seem to have a build system to use.");
        return 0;
    }

    # Ensure we're in a known directory before we start; some options remove
    # the old build directory that a previous module might have been using.
    super_mkdir($pathinfo{'path'});
    p_chdir($pathinfo{'path'});

    return 0 if !$self->setupBuildSystem();
    return 1 if $self->getOption('build-system-only');

    my $buildResults = $buildSystem->buildInternal();
    return 0 if !$buildResults->{was_successful};

    $self->setPersistentOption('last-build-rev', $self->currentScmRevision());

    # TODO: This should be a simple phase to run.
    $self->buildSystem()->runTestsuite()
        if $self->getOption('run-tests');

    if (!$buildResults->{work_done}        &&
        !$self->getOption('refresh-build') &&
        defined $self->getPersistentOption('last-install-rev')
    ) {
        info ("\tNo changes from build, skipping install (--refresh-build this module to force install)");
        return 1;
    } elsif (!$self->getOption('install-after-build')) {
        info ("\tSkipping install due to install-after-build setting");
        return 1;
    }

    # TODO: Likewise this should be a phase to run.
    return $self->install();
}

# Subroutine to setup the build system in a directory.
# Returns boolean true on success, boolean false (0) on failure.
sub setupBuildSystem
{
    my $self = assert_isa(shift, 'ksb::Module');
    my $moduleName = $self->name();

    my $buildSystem = $self->buildSystem();

    if ($buildSystem->name() eq 'generic'
        && $self->hasOption('custom-build-command')
    ) {
        info (" b[*] No build system detected for b[y[$self], assuming custom build command will handle");
        return 1;
    }

    if ($buildSystem->name() eq 'generic' && !pretending()) {
        croak_internal('Build system determination still pending when build attempted.');
    }

    # Check if a previous build has happened in a different directory (which
    # can happen due to name changes on KDE.org side or flat-layout option
    # toggled)
    my $builddir = $self->fullpath('build');
    my $oldBuildDir = $self->getOption('#last-build-dir');
    if (!pretending()             &&
        $builddir ne $oldBuildDir &&
        -d $oldBuildDir           &&
        ! -e $builddir)
    {
        note (" y[b[*] Build directory setting has changed to $builddir.");
        note (" y[b[*] Moving old build directory at $oldBuildDir to the new location.");

        eval {
            Mojo::File->new($oldBuildDir)->move_to($builddir);
        };

        if ($@) {
            warning(<<~MSG);
                r[b[*] Unable to move $oldBuildDir
                r[b[*] to $builddir
                r[b[*] Error: $@
                y[b[*]
                y[b[*] Will proceed, generating a new build dir.
                MSG
        }
    }

    my $refreshReason = $buildSystem->needsRefreshed();
    if ($refreshReason ne "")
    {
        # The build system needs created, either because it doesn't exist, or
        # because the user has asked that it be completely rebuilt.
        info ("\tPreparing build system for y[$self].");

        # Check to see if we're actually supposed to go through the
        # cleaning process.
        if (!$self->getOption('#cancel-clean') &&
            !$buildSystem->cleanBuildSystem())
        {
            warning ("\tUnable to clean r[$self]!");
            return 0;
        }
    }

    my $result = await_result($buildSystem->createBuildSystem());
    if (!$result) {
        error ("\tError creating r[$self]'s build system!");
        return 0;
    }

    # Now we're in the checkout directory
    # So, switch to the build dir.
    # builddir is automatically set to the right value for qt
    p_chdir ($builddir);

    if (!$buildSystem->configureInternal()) {
        error ("\tUnable to configure r[$self] with " . $self->buildSystemType());

        # Add undocumented ".refresh-me" file to build directory to flag
        # for --refresh-build for this module on next run. See also the
        # "needsRefreshed" subroutine.
        if (open my $fh, '>', '.refresh-me') {
            say $fh "# Build directory will be re-generated next kdesrc-build run";
            say $fh "# due to failing to complete configuration on the last run";
            close $fh;
        };

        return 0;
    }

    return 1;
}

# Responsible for installing the module (no update, build, etc.)
# Returns boolean false on failure, boolean true on success.
# Exceptions may be thrown for abnormal conditions (e.g. no build dir exists)
sub install
{
    my $self = assert_isa(shift, 'ksb::Module');
    my $builddir = $self->fullpath('build');
    my $buildSysFile = $self->buildSystem()->configuredModuleFileName();

    if (!pretending() && ! -e "$builddir/$buildSysFile")
    {
        warning ("\tThe build system doesn't exist for r[$self].");
        warning ("\tTherefore, we can't install it. y[:-(].");
        return 0;
    }

    $self->setupEnvironment();

    my @makeInstallOpts = split(' ', $self->getOption('make-install-prefix'));

    # We can optionally uninstall prior to installing
    # to weed out old unused files.
    if ($self->getOption('use-clean-install') &&
        $self->getPersistentOption('last-install-rev'))
    {
        if (!$self->buildSystem()->uninstallInternal(@makeInstallOpts)) {
            warning ("\tUnable to uninstall r[$self] before installing the new build.");
            warning ("\tContinuing anyways...");
        }
        else {
            $self->unsetPersistentOption('last-install-rev');
        }
    }

    if (!$self->buildSystem()->installInternal(@makeInstallOpts))
    {
        error ("\tUnable to install r[$self]!");
        $self->buildContext()->markModulePhaseFailed('install', $self);
        return 0;
    }

    if (pretending())
    {
        pretend ("\tWould have installed g[$self]");
        return 1;
    }

    # Past this point we know we've successfully installed, for real.

    $self->setPersistentOption('last-install-rev', $self->currentScmRevision());

    my $remove_setting = $self->getOption('remove-after-install');

    # Possibly remove the srcdir and builddir after install for users with
    # a little bit of HD space.
    if($remove_setting eq 'all')
    {
        # Remove srcdir
        my $srcdir = $self->fullpath('source');
        note ("\tRemoving b[r[$self source].");
        safe_rmtree($srcdir);
    }

    if($remove_setting eq 'builddir' || $remove_setting eq 'all')
    {
        # Remove builddir
        note ("\tRemoving b[r[$self build directory].");
        safe_rmtree($builddir);

        # We're likely already in the builddir, so chdir back to the root
        p_chdir('/');
    }

    return 1;
}

# Handles uninstalling this module
#
# Returns boolean false on failure, boolean true on success.
sub uninstall
{
    my $self = assert_isa(shift, 'ksb::Module');
    my $builddir = $self->fullpath('build');
    my $buildSysFile = $self->buildSystem()->configuredModuleFileName();

    if (!pretending() && ! -e "$builddir/$buildSysFile")
    {
        warning ("\tThe build system doesn't exist for r[$self].");
        warning ("\tTherefore, we can't uninstall it.");
        return 0;
    }

    $self->setupEnvironment();

    my @makeInstallOpts = split(' ', $self->getOption('make-install-prefix'));

    if (!$self->buildSystem()->uninstallInternal(@makeInstallOpts))
    {
        error ("\tUnable to uninstall r[$self]!");
        $self->buildContext()->markModulePhaseFailed('install', $self);
        return 0;
    }

    if (pretending())
    {
        pretend ("\tWould have uninstalled g[$self]");
        return 1;
    }

    $self->unsetPersistentOption('last-install-rev');
    return 1;
}

sub buildContext
{
    my $self = shift;
    return $self->{context};
}

# Integrates 'set-env' option to the build context environment
sub applyUserEnvironment
{
    my $self = assert_isa(shift, 'ksb::Module');
    my $ctx = $self->buildContext();

    # Let's see if the user has set env vars to be set.
    # Note the global set-env must be checked separately anyways, so
    # we limit inheritance when searching.
    my $env_hash_ref;
    if ($ctx->name() eq $self->name())
    {
        $env_hash_ref = $ctx->getOption("set-env");
    } else {
        $env_hash_ref = $self->getOption("set-env", "module");
    }

    while (my ($key, $value) = each %{$env_hash_ref})
    {
        $ctx->queueEnvironmentVariable($key, $value);
    }
}

# Establishes proper build environment in the build context. Should be run
# before forking off commands for e.g. updates, builds, installs, etc.
sub setupEnvironment ($self)
{
    my $ctx = $self->buildContext();
    my $prefix = $self->installationPath();

    # Add global set-envs and context
    $self->buildContext()->applyUserEnvironment();

    # Build system's environment injection
    my $buildSystem = $self->buildSystem();

    # Suppress injecting qt-install-dir/install-dir related environment variables if a toolchain is also set
    # Let the toolchain files/definitions take care of themselves.
    if ($buildSystem->hasToolchain()) {
        whisper ("\tNot setting environment variables for b[$self]: a custom toolchain is used");
    } else {
        my $installdir   = $self->getOption('install-dir');
        my $qt_installdir    = $self->getOption('qt-install-dir');
        my $libname  = $self->getOption('libname'); # e.g. "lib" or "lib64"

        # Ensure the platform libraries we're building can be found, as long as they
        # are not the system's own libraries.
        for my $platformDir ($qt_installdir, $installdir) {
            next unless $platformDir;       # OK, assume system platform is usable
            next if $platformDir eq '/usr'; # Don't 'fix' things if system platform
                                            # manually set

            $ctx->prependEnvironmentValue('PKG_CONFIG_PATH', "$platformDir/$libname/pkgconfig");
            $ctx->prependEnvironmentValue('LD_LIBRARY_PATH', "$platformDir/$libname");
            $ctx->prependEnvironmentValue('PATH', "$platformDir/bin");
        }

        my $binpath  = $self->getOption('binpath');
        my $libpath  = $self->getOption('libpath');

        $ctx->prependEnvironmentValue('PATH', $binpath)
            if $binpath;
        $ctx->prependEnvironmentValue('LD_LIBRARY_PATH', $libpath)
            if $libpath;
    }

    $buildSystem->prepareModuleBuildEnvironment($ctx, $self, $prefix);

    # Read in user environment defines
    $self->applyUserEnvironment() unless $self == $ctx;
}

# Returns the path to the log directory used during this run for this
# ksb::Module, based on an autogenerated unique id. The id doesn't change
# once generated within a single run of the script.
sub getLogDir
{
    my ($self) = @_;
    return $self->buildContext()->getLogDirFor($self);
}

# Returns a full path that can be open()'d to write a log
# file, based on the given basename (with extension).
# Updates the 'latest' symlink as well, unlike getLogDir
# Use when you know you're going to create a new log
sub getLogPath
{
    my ($self, $path) = @_;
    return $self->buildContext()->getLogPathFor($self, $path);
}

sub toString
{
    my $self = shift;
    return $self->name();
}

sub compare
{
    my ($self, $other) = @_;
    return $self->name() cmp $other->name();
}

sub update
{
    my ($self, $ipc, $ctx) = @_;

    my $moduleName = $self->name();
    my $module_src_dir = $self->getSourceDir();
    my $kdesrc = $ctx->getSourceDir();

    if ($kdesrc ne $module_src_dir)
    {
        # This module has a different source directory, ensure it exists.
        if (!super_mkdir($module_src_dir))
        {
            error ("Unable to create separate source directory for r[$self]: $module_src_dir");
            $ipc->sendIPCMessage(ksb::IPC::MODULE_FAILURE, $moduleName);
        }
    }

    # Check for whether path to source dir has changed due to directory-layout
    # option or changes to metadata.
    my $fullpath = $self->fullpath('source');
    my $oldSourceDir = $self->getOption('#last-source-dir');
    if (!pretending()              &&
        $fullpath ne $oldSourceDir &&
        -d $oldSourceDir           &&
        ! -e $fullpath)
    {
        note (" y[b[*] Source directory setting has changed to $fullpath.");
        note (" y[b[*] Moving old source directory at $oldSourceDir to the new location.");

        eval {
            Mojo::File->new($oldSourceDir)->move_to($fullpath);
        };

        if ($@) {
            warning(<<~MSG);
                r[b[*] Unable to move $oldSourceDir
                r[b[*] to $fullpath
                r[b[*] Error: $@
                y[b[*]
                y[b[*] Will proceed, generating a new source dir.
                MSG
        }
    }

    my $count;
    my $returnValue;

    eval { $count = $self->scm()->updateInternal($ipc) };

    if ($@)
    {
        my $reason = ksb::IPC::MODULE_FAILURE;

        if (had_an_exception()) {
            if ($@->{'exception_type'} eq 'ConflictPresent') {
                $reason = ksb::IPC::MODULE_CONFLICT;
            }
            else {
                $ctx->markModulePhaseFailed('build', $self);
            }

            $@ = $@->{'message'};
        }

        error ("Error updating r[$self], removing from list of packages to build.");
        error (" > y[$@]");

        $ipc->sendIPCMessage($reason, $moduleName);
        $self->phases()->filterOutPhase('build');
        $returnValue = 0;
    }
    else
    {
        my $message;
        if (not defined $count)
        {
            $message = ksb::Debug::colorize ("b[y[Unknown changes].");
            $ipc->notifyUpdateSuccess($moduleName, $message);
        }
        elsif ($count)
        {
            $message = "1 file affected." if $count == 1;
            $message = "$count files affected." if $count != 1;
            $ipc->notifyUpdateSuccess($moduleName, $message);
        }
        else
        {
            $message = "0 files affected.";
            my $refreshReason = $self->buildSystem()->needsRefreshed();

            $ipc->sendIPCMessage(ksb::IPC::MODULE_UPTODATE, "$moduleName,$refreshReason");
        }

        # We doing e.g. --src-only, the build phase that normally outputs
        # number of files updated doesn't get run, so manually mention it
        # here.
        if (!$ipc->supportsConcurrency()) {
            info ("\t$self update complete, $message");
        }

        $returnValue = 1;
    }

    info (""); # Print empty line.
    return $returnValue;
}

# OVERRIDE
#
# This calls OptionsBase::setOption and performs any Module-specific
# handling.
sub setOption
{
    my ($self, %options) = @_;

    # Ensure we don't accidentally get fed module-set options
    for (qw(use-modules ignore-modules)) {
        if (exists $options{$_}) {
            error (" r[b[*] module b[$self] should be declared as module-set to use b[$_]");
            die ksb::BuildException::Config->new($_, "Option $_ can only be used in module-set");
        };
    }

    # Special case handling.
    if (exists $options{'filter-out-phases'}) {
        for my $phase (split(' ', $options{'filter-out-phases'})) {
            $self->phases()->filterOutPhase($phase);
        }
        delete $options{'filter-out-phases'};
    }

    # Phases changes handling
    #
    # The context phases were handled by cmdline. The module-sets will eventually be expanded to modules. For module, we will handle its phases.
    if (exists $options{"no-src"}) {
        $self->phases()->filterOutPhase("update");
        delete $options{"no-src"};
    }
    if (exists $options{"no-install"}) {
        $self->phases()->filterOutPhase("install");
        delete $options{"no-install"};
    }
    if (exists $options{"no-tests"}) {
        $self->phases()->filterOutPhase("test");  # May not work properly yet.
        delete $options{"no-tests"};
    }
    if (exists $options{"no-build"}) {
        $self->phases()->filterOutPhase("build");
        delete $options{"no-build"};
    }
    if (exists $options{"uninstall"}) {
        # Not useful yet. Currently only may be useful to disable uninstallation when uninstalling with cmdline ("uninstall" run_mode)
        if ($self->phases()->has("uninstall")) {
            $self->phases->phases("uninstall");
        } else {
            $self->phases()->clear();
        }
        delete $options{"uninstall"};
    }
    if (exists $options{"build-only"}) {
        if ($self->phases()->has("build")) {
            $self->phases->phases("build");
        } else {
            $self->phases()->clear();
        }
        delete $options{"build-only"};
    }
    if (exists $options{"install-only"}) {
        # Not useful yet, because install is invoked by run_mode or in the end of building function. See a todo with text "Likewise this should be a phase to run."
        if ($self->phases()->has("install")) {
            $self->phases->phases("install");
        } else {
            $self->phases()->clear();
        }
        delete $options{"install-only"};
    }

    $self->SUPER::setOption(%options);
}

# OVERRIDE
#
# This subroutine returns an option value for a given module.  Some globals
# can't be overridden by a module's choice (but see 2nd parameter below).
# If so, the module's choice will be ignored, and a warning will be issued.
#
# Option names are case-sensitive!
#
# Some options (e.g. cmake-options, configure-flags) have the global value
# and then the module's own value appended together. To get the actual
# module setting you must use the level limit parameter set to 'module'.
#
# Likewise, some qt module options do not obey the previous proviso since
# Qt options are not likely to agree nicely with generic KDE buildsystem
# options.
#
# 1st parameter: Name of option
# 2nd parameter: Level limit (optional). If not present, then the value
# 'allow-inherit' is used. Options:
#   - allow-inherit: Module value is used if present (with exceptions),
#     otherwise global is used.
#   - module: Only module value is used (if you want only global then use the
#     buildContext) NOTE: This overrides global "sticky" options as well!
sub getOption
{
    my ($self, $key, $levelLimit) = @_;
    my $ctx = $self->buildContext();
    $levelLimit //= 'allow-inherit';

    # Some global options would probably make no sense applied to Qt.
    my @qtCopyOverrides = qw(branch configure-flags tag cxxflags);
    if (list_has(\@qtCopyOverrides, $key) && $self->buildSystemType() eq 'Qt') {
        $levelLimit = 'module';
    }

    assert_in($levelLimit, [qw(allow-inherit module)]);

    # If module-only, check that first.
    return $self->{options}{$key} if $levelLimit eq 'module';

    my $ctxValue = $ctx->getOption($key); # we'll use this a lot from here

    # Some global options always override module options.
    return $ctxValue if $ctx->hasStickyOption($key);

    # Some options append to the global (e.g. conf flags)
    my @confFlags = qw(cmake-options configure-flags cxxflags);
    if (list_has(\@confFlags, $key) && $ctxValue) {
        return Mojo::Util::trim("$ctxValue " . ($self->{options}{$key} || ''));
    }

    # Everything else overrides the global option, unless it's simply not
    # set at all.
    return $self->{options}{$key} // $ctxValue;
}

# Gets persistent options set for this module. First parameter is the name
# of the option to lookup. Undef is returned if the option is not set,
# although even if the option is set, the value returned might be empty.
# Note that ksb::BuildContext also has this function, with a slightly
# different signature, which OVERRIDEs this function since Perl does not
# have parameter-based method overloading.
sub getPersistentOption
{
    my ($self, $key) = @_;
    return $self->buildContext()->getPersistentOption($self->name(), $key);
}

# Sets a persistent option (i.e. survives between processes) for this module.
# First parameter is the name of the persistent option.
# Second parameter is its actual value.
# See the warning for getPersistentOption above, it also applies for this
# method vs. ksb::BuildContext::setPersistentOption
sub setPersistentOption
{
    my ($self, $key, $value) = @_;
    return $self->buildContext()->setPersistentOption($self->name(), $key, $value);
}

# Unsets a persistent option for this module.
# Only parameter is the name of the option to unset.
sub unsetPersistentOption
{
    my ($self, $key) = @_;
    $self->buildContext()->unsetPersistentOption($self->name(), $key);
}

# Returns the path to the desired directory type (source or build),
# including the module destination directory itself.
sub fullpath
{
    my ($self, $type) = @_;
    assert_in($type, [qw/build source/]);

    my %pathinfo = $self->getInstallPathComponents($type);
    return $pathinfo{'fullpath'};
}

# Returns the "full kde-projects path" for the module. As should be obvious by
# the description, this only works for modules with an scm type that is a
# Updater::KDEProject (or its subclasses), but modules that don't fall into this
# hierarchy will just return the module name (with no path components) anyways.
sub fullProjectPath ($self)
{
    return ($self->getOption('#kde-project-path', 'module') || $self->name());
}

# Returns true if this module is (or was derived from) a kde-projects module.
sub isKDEProject ($self)
{
    return $self->scmType() eq 'proj';
}

# Subroutine to return the name of the destination directory for the
# checkout and build routines.  Based on the dest-dir option.  The return
# value will be relative to the src/build dir.  The user may use the
# '$MODULE' or '${MODULE}' sequences, which will be replaced by the name of
# the module in question.
sub destDir ($self)
{
    my $destDir = $self->getOption('dest-dir');
    my $basePath;

    my $layout = $self->getOption('directory-layout');
    if ($layout eq 'flat') {
        $basePath = $self->name();
    } elsif ($layout eq 'invent') { # invent layout is the modern layout for proper KDE projects
        $basePath = $self->getOption('#kde-repo-path', 'module');
        $basePath ||= $self->name(); # Default if not provided in repo-metadata
    } elsif ($layout eq 'metadata') {
        $basePath = $self->getOption('#kde-project-path', 'module');
        $basePath ||= $self->name(); # Default if not provided in repo-metadata
    } else {
        if (!$self->hasOption('#warned-invalid-directory-layout')) # avoid spamming
        {
            warning ("y[ * Invalid b[directory-layout]y[ value: \"$layout\". Will use b[flat]y[ instead for b[$self]");
            $self->setOption('#warned-invalid-directory-layout', 1);
        }
        $basePath = $self->name();
    }

    # Note the default dest-dir option is '${MODULE}' so this normally is used
    $destDir =~ s/(\$\{MODULE})|(\$MODULE\b)/$basePath/g;

    return $destDir;
}

# Subroutine to return the installation path of a given module (the value
# that is passed to the CMAKE_INSTALL_PREFIX CMake option).
# It is based on the "install-dir" option.
# The user may use '$MODULE' or '${MODULE}' in the "install-dir" option to have
# them replaced by the name of the module in question.
sub installationPath
{
    my $self = assert_isa(shift, 'ksb::Module');
    my $path = $self->getOption('install-dir');
    my $moduleName = $self->name();
    $path =~ s/(\$\{MODULE})|(\$MODULE\b)/$moduleName/g;

    return $path;
}

# Returns a list of any 'post-build' messages that have been set for the module
# to show after the build has ended. These may be messages such as warning of a
# local source conflict that may have scrolled past or similar things the user
# needs to know about.
#
# Each entry in the list will be a text message that should be shown (perhaps
# with additional formatting).
sub getPostBuildMessages
{
    my $self = assert_isa(shift, 'ksb::Module');
    return @{$self->{post_build_msgs}};
}

# Adds the given message to the list of post-build messages to show to the user
sub addPostBuildMessage
{
    my ($self, $new_msg) = @_;

    push @{$self->{post_build_msgs}}, $new_msg;
    return;
}

1;
