diff --git a/modules/ksb/BuildSystem.pm b/modules/ksb/BuildSystem.pm index 6dba496..f8e1b93 100644 --- a/modules/ksb/BuildSystem.pm +++ b/modules/ksb/BuildSystem.pm @@ -1,506 +1,522 @@ package ksb::BuildSystem 0.30; # Base module for the various build systems, includes built-in implementations of # generic functions and supports hooks for subclasses to provide needed detailed # functionality. use strict; use warnings; use 5.014; use ksb::BuildException; use ksb::Debug; use ksb::Util; use ksb::StatusView; use List::Util qw(first); sub new { my ($class, $module) = @_; my $self = bless { module => $module }, $class; # This is simply the 'default' build system at this point, also used for # KF5. if ($class ne 'ksb::BuildSystem::KDE4') { _maskGlobalBuildSystemOptions($self); } return $self; } # Removes or masks global build system-related options, so that they aren't # accidentally picked up for use with our non-default build system. # Module-specific options are left intact. sub _maskGlobalBuildSystemOptions { my $self = shift; my $module = $self->module(); my $ctx = $module->buildContext(); my @buildSystemOptions = qw( cmake-options cmake-generator configure-flags custom-build-command cxxflags make-options run-tests use-clean-install ); for my $opt (@buildSystemOptions) { # If an option is present, and not set at module-level, it must be # global. Can't use getOption() method due to recursion. if ($ctx->{options}->{$opt} && !$module->{options}->{$opt}) { $module->{options}->{$opt} = ''; } } } +# +# Check if a (custom) toolchain is defined. +# If a build system is configured with a (custom) toolchain, it is assumed that +# +# a: the user knows what they are doing, or +# b: they are using an SDK that knows what it is about +# +# In either case, kdesrc-build will avoid touching the environment variables to +# give the custom configuration maximum 'power' (including foot shooting power). +# +sub hasToolchain +{ + my $self = shift; + return 0; +} + sub module { my $self = shift; return $self->{module}; } # Subroutine to determine if a given module needs to have the build system # recreated from scratch. # If so, it returns a non empty string sub needsRefreshed { my $self = assert_isa(shift, 'ksb::BuildSystem'); my $module = $self->module(); my $builddir = $module->fullpath('build'); my $confFileKey = $self->configuredModuleFileName(); if (not -e "$builddir") { return "the build directory doesn't exist"; } if (-e "$builddir/.refresh-me") { return "the last configure failed"; # see Module.pm } if ($module->getOption("refresh-build")) { return "the option refresh-build was set"; } if (not -e "$builddir/$confFileKey") { return "$builddir/$confFileKey is missing"; } return ""; } # Returns true if the given subdirectory (reference from the module's root source directory) # can be built or not. Should be reimplemented by subclasses as appropriate. sub isSubdirBuildable { return 1; } # Called by the module being built before it runs its build/install process. Should # setup any needed environment variables, build context settings, etc., in preparation -# for the build and install phases. +# for the build and install phases. Should take `hasToolchain()` into account here. sub prepareModuleBuildEnvironment { my ($self, $ctx, $module, $prefix) = @_; } # Returns true if the module should have make install run in order to be # used, or false if installation is not required or possible. sub needsInstalled { return 1; } # This should return a list of executable names that must be present to # even bother attempting to use this build system. An empty list should be # returned if there's no required programs. sub requiredPrograms { return; } sub name { return 'generic'; } # Returns a list of possible build commands to run, any one of which should # be supported by the build system. sub buildCommands { # Non Linux systems can sometimes fail to build when GNU Make would work, # so prefer GNU Make if present, otherwise try regular make. return 'gmake', 'make'; } sub defaultBuildCommand { my $self = shift; # Convert the path to an absolute path since I've encountered a sudo # that is apparently unable to guess. Maybe it's better that it # doesn't guess anyways from a security point-of-view. my $buildCommand = first { absPathToExecutable($_) } $self->buildCommands(); return $buildCommand; } # Return value style: boolean sub buildInternal { my $self = shift; my $optionsName = shift // 'make-options'; return $self->safe_make({ target => undef, message => 'Compiling...', 'make-options' => [ split(' ', $self->module()->getOption($optionsName)), ], logbase => 'build', subdirs => [ split(' ', $self->module()->getOption("checkout-only")) ], })->{was_successful}; } # Return value style: boolean sub configureInternal { # It is possible to make it here if there's no source dir and if we're # pretending. If we're not actually pretending then this should be a # bug... return 1 if pretending(); croak_internal('We were not supposed to get to this point...'); } # Returns name of file that should exist (relative to the module's build directory) # if the module has been configured. sub configuredModuleFileName { my $self = shift; return 'Makefile'; } # Runs the testsuite for the given module. # Returns true if a testsuite is present and all tests passed, false otherwise. sub runTestsuite { my $self = shift; my $module = $self->module(); info ("\ty[$module] does not support the b[run-tests] option"); return 0; } # Used to install a module (that has already been built, tested, etc.) # All options passed are prefixed to the eventual command to be run. # Returns boolean false if unable to install, true otherwise. sub installInternal { my $self = shift; my $module = $self->module(); my @cmdPrefix = @_; return $self->safe_make ({ target => 'install', message => 'Installing..', 'prefix-options' => [@cmdPrefix], subdirs => [ split(' ', $module->getOption("checkout-only")) ], })->{was_successful}; } # Used to uninstall a previously installed module. # All options passed are prefixed to the eventual command to be run. # Returns boolean false if unable to uninstall, true otherwise. sub uninstallInternal { my $self = shift; my $module = $self->module(); my @cmdPrefix = @_; return $self->safe_make ({ target => 'uninstall', message => "Uninstalling g[$module]", 'prefix-options' => [@cmdPrefix], subdirs => [ split(' ', $module->getOption("checkout-only")) ], })->{was_successful}; } # Subroutine to clean the build system for the given module. Works by # recursively deleting the directory and then recreating it. # Returns 0 for failure, non-zero for success. sub cleanBuildSystem { my $self = assert_isa(shift, 'ksb::BuildSystem'); my $module = $self->module(); my $srcdir = $module->fullpath('source'); my $builddir = $module->fullpath('build'); if (pretending()) { pretend ("\tWould have cleaned build system for g[$module]"); return 1; } # Use an existing directory if (-e $builddir && $builddir ne $srcdir) { info ("\tRemoving files in build directory for g[$module]"); # This variant of log_command runs the sub prune_under_directory($builddir) # in a forked child, so that we can log its output. if (log_command($module, 'clean-builddir', [ 'kdesrc-build', 'main::prune_under_directory', $builddir ])) { error (" r[b[*]\tFailed to clean build directory. Verify the permissions are correct."); return 0; # False for this function. } # Let users know we're done so they don't wonder why rm -rf is taking so # long and oh yeah, why's my HD so active?... info ("\tOld build system cleaned, starting new build system."); } # or create the directory elsif (!super_mkdir ($builddir)) { error ("\tUnable to create directory r[$builddir]."); return 0; } return 1; } sub needsBuilddirHack { return 0; # By default all build systems are assumed to be sane } # Return convention: boolean sub createBuildSystem { my $self = assert_isa(shift, 'ksb::BuildSystem'); my $module = $self->module(); my $builddir = $module->fullpath('build'); my $srcdir = $module->fullpath('source'); if (! -e "$builddir" && !super_mkdir("$builddir")) { error ("\tUnable to create build directory for r[$module]!!"); return 0; } if ($builddir ne $srcdir && $self->needsBuilddirHack() && 0 != log_command($module, 'lndir', ['kdesrc-build', 'main::safe_lndir', $srcdir, $builddir])) { error ("\tUnable to setup symlinked build directory for r[$module]!!"); return 0; } return 1; } # Subroutine to run the build command with the arguments given by the # passed hash. # # In addition to finding the proper executable, this function handles the # step of running the build command for individual subdirectories (as # specified by the checkout-only option to the module). Due to the various # ways the build command is called by this script, it is required to pass # customization options in a hash: # { # target => undef, or a valid build target e.g. 'install', # message => 'Compiling.../Installing.../etc.' # make-options => [ list of command line arguments to pass to make. See # make-options ], # prefix-options => [ list of command line arguments to prefix *before* the # make command, used for make-install-prefix support for # e.g. sudo ], # logbase => 'base-log-filename', # subdirs => [ list of subdirectories of the module to build, # relative to the module's own build directory. ] # } # # target and message are required. logbase is required if target is left # undefined, but otherwise defaults to the same value as target. # # Note that the make command is based on the results of the 'buildCommands' # subroutine which should be overridden if necessary by subclasses. Each # command should be the command name (i.e. no path). The user may override # the command used (for build only) by using the 'custom-build-command' # option. # # The first command name found which resolves to an executable on the # system will be used, if no command this function will fail. # # The first argument should be the ksb::Module object to be made. # The second argument should be the reference to the hash described above. # # Returns a hashref: # { # was_successful => $bool, (if successful) # } sub safe_make (@) { my ($self, $optsRef) = @_; assert_isa($self, 'ksb::BuildSystem'); my $module = $self->module(); my $buildCommand = $self->defaultBuildCommand(); my @buildCommandLine = $buildCommand; # Check for custom user command. We support command line options being # passed to the command as well. my $userCommand = $module->getOption('custom-build-command'); if ($userCommand) { @buildCommandLine = split_quoted_on_whitespace($userCommand); $buildCommand = absPathToExecutable($buildCommandLine[0]); } if (!$buildCommand) { $buildCommand = $userCommand || $self->buildCommands(); error (" r[b[*] Unable to find the g[$buildCommand] executable!"); return { was_successful => 0 }; } # Make it prettier if pretending (Remove leading directories). $buildCommand =~ s{^/.*/}{} if pretending(); shift @buildCommandLine; # $buildCommand is already the first entry. # Simplify code by forcing lists to exist. $optsRef->{'prefix-options'} //= [ ]; $optsRef->{'make-options'} //= [ ]; $optsRef->{'subdirs'} //= [ ]; my @prefixOpts = @{$optsRef->{'prefix-options'}}; # If using sudo ensure that it doesn't wait on tty, but tries to read from # stdin (which should fail as we redirect that from /dev/null) if (@prefixOpts && $prefixOpts[0] eq 'sudo' && !grep { /^-S$/ } @prefixOpts) { splice (@prefixOpts, 1, 0, '-S'); # Add -S right after 'sudo' } # Assemble arguments my @args = (@prefixOpts, $buildCommand, @buildCommandLine); push @args, $optsRef->{target} if $optsRef->{target}; push @args, @{$optsRef->{'make-options'}}; # Will be output by _runBuildCommand my $buildMessage = $optsRef->{message}; # Here we're attempting to ensure that we either run the build command # in each subdirectory, *or* for the whole module, but not both. my @dirs = @{$optsRef->{subdirs}}; push (@dirs, "") if scalar @dirs == 0; for my $subdir (@dirs) { # Some subdirectories shouldn't have the build command run within # them. next unless $self->isSubdirBuildable($subdir); my $logname = $optsRef->{logbase} // $optsRef->{logfile} // $optsRef->{target}; if ($subdir ne '') { $logname = $logname . "-$subdir"; # Remove slashes in favor of something else. $logname =~ tr{/}{-}; # Mention subdirectory that we're working on, move ellipsis # if present. if ($buildMessage =~ /\.\.\.$/) { $buildMessage =~ s/(\.\.\.)?$/ subdirectory g[$subdir]$1/; } } my $builddir = $module->fullpath('build') . "/$subdir"; $builddir =~ s/\/*$//; # Remove trailing / p_chdir ($builddir); return $self->_runBuildCommand($buildMessage, $logname, \@args); }; return { was_successful => 1 }; } # Subroutine to run make and process the build process output in order to # provide completion updates. This procedure takes the same arguments as # log_command() (described here as well), except that the callback argument # is not used. # # First parameter is the message to display to the user while the build # happens. # Second parameter is the name of the log file to use (relative to the log # directory). # Third parameter is a reference to an array with the command and its # arguments. i.e. ['command', 'arg1', 'arg2'] # # The return value is a hashref as defined by safe_make sub _runBuildCommand { my ($self, $message, $filename, $argRef) = @_; my $module = $self->module(); my $resultRef = { was_successful => 0 }; my $ctx = $module->buildContext(); # There are situations when we don't want progress output: # 1. If we're not printing to a terminal. # 2. When we're debugging (we'd interfere with debugging output). if (! -t STDERR || debugging()) { note("\t$message"); $resultRef->{was_successful} = (0 == log_command($module, $filename, $argRef)); return $resultRef; } my $time = time; my $statusViewer = $ctx->statusViewer(); $statusViewer->setStatus("\t$message"); $statusViewer->update(); # TODO More details my $warnings = 0; # w00t. Check out the closure! Maks would be so proud. my $log_command_callback = sub { my $input = shift; return if not defined $input; my ($percentage) = ($input =~ /^\[\s*([0-9]+)%]/); if ($percentage) { $statusViewer->setProgressTotal(100); $statusViewer->setProgress($percentage); } else { my ($x, $y) = ($input =~ /^\[([0-9]+)\/([0-9]+)] /); if ($x && $y) { # ninja-syntax $statusViewer->setProgressTotal($y); $statusViewer->setProgress($x); } } $warnings++ if ($input =~ /warning: /); }; $resultRef->{was_successful} = (0 == log_command($module, $filename, $argRef, { callback => $log_command_callback })); $resultRef->{warnings} = $warnings; # Cleanup TTY output. $time = prettify_seconds(time - $time); my $status = $resultRef->{was_successful} ? "g[b[succeeded]" : "r[b[failed]"; $statusViewer->releaseTTY("\t$message $status (after $time)\n"); if ($warnings) { my $count = ($warnings < 3 ) ? 1 : ($warnings < 10 ) ? 2 : ($warnings < 30 ) ? 3 : 4; my $msg = sprintf("%s b[y[$warnings] %s", '-' x $count, '-' x $count); note ("\tNote: $msg compile warnings"); $self->{module}->setPersistentOption('last-compile-warnings', $warnings); } return $resultRef; } 1; diff --git a/modules/ksb/BuildSystem/KDE4.pm b/modules/ksb/BuildSystem/KDE4.pm index 6ddc5f3..705d272 100644 --- a/modules/ksb/BuildSystem/KDE4.pm +++ b/modules/ksb/BuildSystem/KDE4.pm @@ -1,393 +1,473 @@ package ksb::BuildSystem::KDE4 0.20; # Class responsible for building KDE4 CMake-based modules. use strict; use warnings; use 5.014; use parent qw(ksb::BuildSystem); use ksb::BuildContext 0.30; use ksb::Debug; use ksb::Util; use List::Util qw(first); my $GENERATOR_MAP = { 'Ninja' => { optionsName => 'ninja-options', installTarget => 'install', requiredPrograms => [ qw{ninja cmake qmake} ], buildCommands => [ qw{ninja} ] }, 'Unix Makefiles' => { optionsName => 'make-options', installTarget => 'install/fast', requiredPrograms => [ qw{cmake qmake} ], # Non Linux systems can sometimes fail to build when GNU Make would work, # so prefer GNU Make if present, otherwise try regular make. buildCommands => [ qw{gmake make} ] } }; sub _checkGeneratorIsWhitelisted { my $generator = shift; return exists ($GENERATOR_MAP->{$generator}); } sub _stripGeneratorFromCMakeOptions { my $nextShouldBeGenerator = 0; my @filtered = grep { my $accept = 1; if ($nextShouldBeGenerator) { $nextShouldBeGenerator = 0; $accept = 0; } else { my $maybeGenerator = $_; if ($maybeGenerator =~ /^-G(\S*(\s*\S)*)\s*/) { my $generator = $1 // ''; $nextShouldBeGenerator = 1 if ($generator eq ''); $accept = 0; } } $accept == 1; } (@_); return @filtered; } sub _findGeneratorInCMakeOptions { my $nextShouldBeGenerator = 0; my @filtered = grep { my $accept = 0; if ($nextShouldBeGenerator) { $nextShouldBeGenerator = 0; $accept = 1; } else { my $maybeGenerator = $_; if ($maybeGenerator =~ /^-G(\S*(\s*\S)*)\s*/) { my $generator = $1 // ''; if ($generator ne '') { $accept = 1; } else { $nextShouldBeGenerator = 1; } } } $accept == 1; } (@_); for my $found (@filtered) { if ($found =~ /^-G(\S*(\s*\S)*)\s*/) { $found = $1 // ''; } return $found unless ($found eq ''); } return ''; } +sub _checkToolchainOk +{ + my $toolchain = shift; + return $toolchain ne '' && -f $toolchain && -r $toolchain; +} + +sub _stripToolchainFromCMakeOptions +{ + my @filtered = grep { + my $accept = 1; + my $maybeToolchain = $_; + if ($maybeToolchain =~ /^-DCMAKE_TOOLCHAIN_FILE=(\S*(\s*\S)*)\s*/) { + $accept = 0; + } + + $accept == 1; + } (@_); + return @filtered; +} + +sub _findToolchainInCMakeOptions +{ + my $found = first { + my $accept = 0; + my $maybeToolchain = $_; + if ($maybeToolchain =~ /^-DCMAKE_TOOLCHAIN_FILE=(\S*(\s*\S)*)\s*/) { + my $file = $1 // ''; + $accept = 1 if (_checkToolchainOk($file)); + } + + $accept == 1; + } (@_); + + if ($found && $found =~ /^-DCMAKE_TOOLCHAIN_FILE=(\S*(\s*\S)*)\s*/) { + $found = $1 // ''; + return $found if (_checkToolchainOk($found)); + } + + return ''; +} + +sub _determineCmakeToolchain +{ + my $self = shift; + + my $module = $self->module(); + my @cmakeOptions = split_quoted_on_whitespace ($module->getOption('cmake-options')); + + my $toolchain = first { _checkToolchainOk($_); } ( + _findToolchainInCMakeOptions(@cmakeOptions), + $module->getOption('cmake-toolchain') + ); + + return $toolchain // ''; +} + +sub cmakeToolchain +{ + my $self = shift; + if (not (exists $self->{cmake_toolchain})) { + $self->{cmake_toolchain} = $self->_determineCmakeToolchain(); + } + return $self->{cmake_toolchain}; +} + +sub hasToolchain +{ + my $self = shift; + return $self->cmakeToolchain() ne ''; +} + sub _determineCmakeGenerator { my $self = shift; my $module = $self->module(); my @cmakeOptions = split_quoted_on_whitespace ($module->getOption('cmake-options')); my $generator = first { _checkGeneratorIsWhitelisted($_); } ( _findGeneratorInCMakeOptions(@cmakeOptions), $module->getOption('cmake-generator'), 'Unix Makefiles' ); croak_internal("Unable to determine CMake generator for: $module") unless $generator; return $generator; } sub cmakeGenerator { my $self = shift; if (not (exists $self->{cmake_generator})) { $self->{cmake_generator} = $self->_determineCmakeGenerator(); } return $self->{cmake_generator}; } - sub needsInstalled { my $self = shift; return 0 if $self->name() eq 'kde-common'; # Vestigial return 1; } sub name { return 'KDE'; } # Called by the module being built before it runs its build/install process. Should # setup any needed environment variables, build context settings, etc., in preparation # for the build and install phases. sub prepareModuleBuildEnvironment { my ($self, $ctx, $module, $prefix) = @_; + # + # Suppress injecting qtdir/kdedir related environment variables if a toolchain is also set + # Let the toolchain files/definitions take care of themselves. + # + return if $self->hasToolchain(); + # Avoid moving /usr up in env vars if ($prefix ne '/usr') { # Find the normal CMake "config" mode files for find_package() $ctx->prependEnvironmentValue('CMAKE_PREFIX_PATH', $prefix); # Try to ensure that older "module" mode find_package() calls also point to right directory $ctx->prependEnvironmentValue('CMAKE_MODULE_PATH', "$prefix/lib64/cmake:$prefix/lib/cmake"); $ctx->prependEnvironmentValue('XDG_DATA_DIRS', "$prefix/share"); } my $qtdir = $module->getOption('qtdir'); if ($qtdir && $qtdir ne $prefix) { # Ensure we can find Qt5's own CMake modules $ctx->prependEnvironmentValue('CMAKE_PREFIX_PATH', $qtdir); $ctx->prependEnvironmentValue('CMAKE_MODULE_PATH', "$qtdir/lib/cmake"); } } # This should return a list of executable names that must be present to # even bother attempting to use this build system. An empty list should be # returned if there's no required programs. sub requiredPrograms { my $self = shift; my $generator = $self->cmakeGenerator(); my @required = @{$GENERATOR_MAP->{$generator}->{requiredPrograms}}; return @required; } # Returns a list of possible build commands to run, any one of which should # be supported by the build system. sub buildCommands { my $self = shift; my $generator = $self->cmakeGenerator(); my @progs = @{$GENERATOR_MAP->{$generator}->{buildCommands}}; return @progs; } sub configuredModuleFileName { my $self = shift; return 'cmake_install.cmake'; } sub runTestsuite { my $self = assert_isa(shift, 'ksb::BuildSystem::KDE4'); my $module = $self->module(); # Note that we do not run safe_make, which should really be called # safe_compile at this point. # Step 1: Ensure the tests are built, oh wait we already did that when we ran # CMake :) my $make_target = 'test'; if ($module->getOption('run-tests') eq 'upload') { $make_target = 'Experimental'; } info ("\tRunning test suite..."); # Step 2: Run the tests. my $numTests = -1; my $countCallback = sub { if ($_ && /([0-9]+) tests failed out of/) { $numTests = $1; } }; my $buildCommand = $self->defaultBuildCommand(); my $result = log_command($module, 'test-results', [ $buildCommand, $make_target ], { callback => $countCallback, no_translate => 1}); if ($result != 0) { my $logDir = $module->getLogDir(); if ($numTests > 0) { warning ("\t$numTests tests failed for y[$module], consult $logDir/test-results.log for info"); } else { warning ("\tSome tests failed for y[$module], consult $logDir/test-results.log for info"); } return 0; } else { info ("\tAll tests ran successfully."); } return 1; } # Re-implementing the one in BuildSystem since in CMake we want to call # make install/fast, so it only installs rather than building + installing sub installInternal { my $self = shift; my $module = $self->module(); my $generator = $self->cmakeGenerator(); my $target = $GENERATOR_MAP->{$generator}->{installTarget}; my @cmdPrefix = @_; $target = 'install' if $module->getOption('custom-build-command'); return $self->safe_make ({ target => $target, logfile => 'install', message => 'Installing..', 'prefix-options' => [@cmdPrefix], subdirs => [ split(' ', $module->getOption("checkout-only")) ], })->{was_successful}; } sub configureInternal { my $self = assert_isa(shift, 'ksb::BuildSystem::KDE4'); my $module = $self->module(); # Use cmake to create the build directory (sh script return value # semantics). if ($self->_safe_run_cmake()) { error ("\tUnable to configure r[$module] with CMake!"); return 0; } return 1; } # Return value style: boolean sub buildInternal { my $self = shift; my $generator = $self->cmakeGenerator(); my $defaultOptionsName = $GENERATOR_MAP->{$generator}->{optionsName}; my $optionsName = shift // "$defaultOptionsName"; return $self->safe_make({ target => undef, message => 'Compiling...', 'make-options' => [ split(' ', $self->module()->getOption($optionsName)), ], logbase => 'build', subdirs => [ split(' ', $self->module()->getOption("checkout-only")) ], })->{was_successful}; } ### Internal package functions. # Subroutine to run CMake to create the build directory for a module. # CMake is not actually run if pretend mode is enabled. # # First parameter is the module to run cmake on. # Return value is the shell return value as returned by log_command(). i.e. # 0 for success, non-zero for failure. sub _safe_run_cmake { my $self = shift; my $module = $self->module(); my $generator = $self->cmakeGenerator(); + my $toolchain = $self->cmakeToolchain(); my $srcdir = $module->fullpath('source'); my @commands = split_quoted_on_whitespace ($module->getOption('cmake-options')); # grep out empty fields @commands = grep {!/^\s*$/} @commands; @commands = _stripGeneratorFromCMakeOptions(@commands); + @commands = _stripToolchainFromCMakeOptions(@commands); + + unshift @commands, "-DCMAKE_TOOLCHAIN_FILE=$toolchain" if $toolchain ne ''; # Add -DBUILD_foo=OFF options for the directories in do-not-compile. # This will only work if the CMakeLists.txt file uses macro_optional_add_subdirectory() my @masked_directories = split(' ', $module->getOption('do-not-compile')); push @commands, "-DBUILD_$_=OFF" foreach @masked_directories; # Get the user's CXXFLAGS, use them if specified and not already given # on the command line. my $cxxflags = $module->getOption('cxxflags'); if ($cxxflags and not grep { /^-DCMAKE_CXX_FLAGS(:\w+)?=/ } @commands) { push @commands, "-DCMAKE_CXX_FLAGS:STRING=$cxxflags"; } my $prefix = $module->installationPath(); push @commands, "-DCMAKE_INSTALL_PREFIX=$prefix"; # Add custom Qt to the prefix (but don't overwrite a user-set prefix) my $qtdir = $module->getOption('qtdir'); if ($qtdir && $qtdir ne $prefix && !grep { /^\s*-DCMAKE_PREFIX_PATH/ } (@commands) ) { push @commands, "-DCMAKE_PREFIX_PATH=$qtdir"; } if ($module->getOption('run-tests') && !grep { /^\s*-DKDE4_BUILD_TESTS(:BOOL)?=(ON|TRUE|1)\s*$/ } (@commands) ) { whisper ("Enabling tests"); push @commands, "-DKDE4_BUILD_TESTS:BOOL=ON"; # Also enable phonon tests. if ($module =~ /^phonon$/) { push @commands, "-DPHONON_BUILD_TESTS:BOOL=ON"; } } if ($module->getOption('run-tests') eq 'upload') { whisper ("Enabling upload of test results"); push @commands, "-DBUILD_experimental:BOOL=ON"; } unshift @commands, 'cmake', $srcdir, '-G', $generator; # Add to beginning of list. my $old_options = $module->getPersistentOption('last-cmake-options') || ''; my $builddir = $module->fullpath('build'); if (($old_options ne get_list_digest(@commands)) || $module->getOption('reconfigure') || ! -e "$builddir/CMakeCache.txt" # File should exist only on successful cmake run ) { info ("\tRunning g[cmake] targeting b[$generator]..."); # Remove any stray CMakeCache.txt safe_unlink ("$srcdir/CMakeCache.txt") if -e "$srcdir/CMakeCache.txt"; safe_unlink ("$builddir/CMakeCache.txt") if -e "$builddir/CMakeCache.txt"; $module->setPersistentOption('last-cmake-options', get_list_digest(@commands)); return log_command($module, "cmake", \@commands); } # Skip cmake run return 0; } 1; diff --git a/modules/ksb/Module.pm b/modules/ksb/Module.pm index c53e6d5..bac7202 100644 --- a/modules/ksb/Module.pm +++ b/modules/ksb/Module.pm @@ -1,983 +1,993 @@ package ksb::Module 0.20; # Class: Module # # Represents a source code module of some sort, which can be updated, built, # and installed. Includes a stringifying overload and can be sorted amongst # other ksb::Modules. use 5.014; use warnings; no if $] >= 5.018, 'warnings', 'experimental::smartmatch'; use parent qw(ksb::OptionsBase); use ksb::IPC; use ksb::Debug; use ksb::Util; use ksb::l10nSystem; use ksb::Updater::Svn; use ksb::Updater::Git; use ksb::Updater::Bzr; 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::Qt4; use ksb::BuildSystem::Qt5; use ksb::BuildSystem::KDE4; use ksb::BuildSystem::CMakeBootstrap; use ksb::BuildSystem::Meson; use ksb::ModuleSet::Null; 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. my $phases = dclone($ctx->phases()) if $ctx; my %newOptions = ( name => $name, scm_obj => undef, build_obj => undef, phases => $phases, context => $ctx, 'module-set' => undef, ); @{$self}{keys %newOptions} = values %newOptions; 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}"; my $compatDestDir = $module->destDir($module->name()); my $fullCompatPath = "$srcbase/$compatDestDir"; # 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}; # Look for specific setting of repository and svn-server. If both is # set it's a bug, if one is set, that's the type (because the user says # so...). Don't use getOption($key) as it will try to fallback to # global options. my $svn_status = $self->getOption('svn-server', 'module'); my $repository = $self->getOption('repository', 'module') // ''; my $rcfile = $self->buildContext()->rcFile(); if ($svn_status && $repository) { error (<{scm_obj} = ksb::Updater::Bzr->new($self); } # If it needs a repo it's git. Everything else is svn for now. $self->{scm_obj} //= $repository ? ksb::Updater::Git->new($self) : ksb::Updater::Svn->new($self); return $self->{scm_obj}; } sub setScmType { my ($self, $scmType) = @_; my $newType; given($scmType) { when('git') { $newType = ksb::Updater::Git->new($self); } when('proj') { $newType = ksb::Updater::KDEProject->new($self); } when('metadata') { $newType = ksb::Updater::KDEProjectMetadata->new($self); } when('l10n') { $newType = ksb::l10nSystem->new($self); } when('svn') { $newType = ksb::Updater::Svn->new($self); } when('bzr') { $newType = ksb::Updater::Bzr->new($self); } when('qt5') { $newType = ksb::Updater::Qt5->new($self); } default { $newType = undef; } } $self->{scm_obj} = $newType; } # Returns a string describing the scm platform of the given module. # Return value: 'git' or 'svn' 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', 'cmake-bootstrap' => 'ksb::BuildSystem::CMakeBootstrap', 'kde' => 'ksb::BuildSystem::KDE4', 'qt' => 'ksb::BuildSystem::Qt4', 'qt5' => 'ksb::BuildSystem::Qt5', 'autotools' => 'ksb::BuildSystem::Autotools', 'meson' => 'ksb::BuildSystem::Meson', ); my $class = $buildSystemClasses{lc $name} // undef; return $class->new($self) if ($class); # Past here, no class found croak_runtime("Invalid build system $name requested"); } 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'); if (($self->getOption('repository') =~ /gitorious\.org\/qt\//) || ($self->getOption('repository') =~ /^kde:qt$/) || (-e "$sourceDir/bin/syncqt")) { $buildType = ksb::BuildSystem::Qt4->new($self); } # 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->getOption('#xml-full-path'))) { $buildType = ksb::BuildSystem::KDE4->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, l10n (KDE language buildsystem), 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()) { 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'); if (!$buildSystem->buildInternal()) { return 0; } $self->setPersistentOption('last-build-rev', $self->currentScmRevision()); # TODO: This should be a simple phase to run. if ($self->getOption('run-tests')) { $self->buildSystem()->runTestsuite(); } # TODO: Likewise this should be a phase to run. if ($self->getOption('install-after-build')) { return 0 if !$self->install(); } else { info ("\tSkipping install for y[$self]"); } return 1; } # 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' && !pretending()) { croak_internal('Build system determination still pending when build attempted.'); } 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; } } if (!$buildSystem->createBuildSystem()) { 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 ($self->fullpath('build')); 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.) # Return value: Boolean flag indicating whether module installed successfully or # not. # 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 (or its sub-directories as given by the checkout-only # option). # # Returns boolean false on failure, boolean true otherwise. 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 = $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 { my $self = assert_isa(shift, 'ksb::Module'); my $ctx = $self->buildContext(); - my $kdedir = $self->getOption('kdedir'); - my $qtdir = $self->getOption('qtdir'); my $prefix = $self->installationPath(); # Add global set-envs and context $self->buildContext()->applyUserEnvironment(); - # Ensure the platform libraries we're building can be found, as long as they - # are not the system's own libraries. - for my $platformDir ($qtdir, $kdedir) { - next unless $platformDir; # OK, assume system platform is usable - next if $platformDir eq '/usr'; # Don't 'fix' things if system platform - # manually set + # Build system's environment injection + my $buildSystem = $self->buildSystem(); - $ctx->prependEnvironmentValue('PKG_CONFIG_PATH', "$platformDir/lib/pkgconfig"); - $ctx->prependEnvironmentValue('LD_LIBRARY_PATH', "$platformDir/lib"); - $ctx->prependEnvironmentValue('PATH', "$platformDir/bin"); + # + # Suppress injecting qtdir/kdedir related environment variables if a toolchain is also set + # Let the toolchain files/definitions take care of themselves. + # + if ($buildSystem->hasToolchain()) { + note ("\tNot setting environment variables for b[$self]: a custom toolchain is used"); + } else { + my $kdedir = $self->getOption('kdedir'); + my $qtdir = $self->getOption('qtdir'); + + # Ensure the platform libraries we're building can be found, as long as they + # are not the system's own libraries. + for my $platformDir ($qtdir, $kdedir) { + 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/lib/pkgconfig"); + $ctx->prependEnvironmentValue('LD_LIBRARY_PATH', "$platformDir/lib"); + $ctx->prependEnvironmentValue('PATH', "$platformDir/bin"); + } } - # Build system's environment injection - my $buildSystem = $self->buildSystem(); $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); next; } } my $fullpath = $self->fullpath('source'); 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(git-repository-base 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'}; } $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 trimmed("$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 { my $self = shift; return ($self->getOption('#xml-full-path', 'module') || $self->name()); } # Returns true if this module is (or was derived from) a kde-projects module. sub isKDEProject { my $self = shift; return $self->hasOption('#xml-full-path'); } # 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. # # The first parameter is optional, but if provided will be used as the base # path to replace $MODULE entries in dest-dir. sub destDir { my $self = assert_isa(shift, 'ksb::Module'); my $destDir = $self->getOption('dest-dir'); my $basePath = ""; if ($self->getOption('ignore-kde-structure')) { $basePath = $self->name(); } else { $basePath = shift // $self->getOption('#xml-full-path'); $basePath ||= $self->name(); # Default if not provided in repo-metadata } $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 "prefix" and, if it is not set, the "kdedir" option. # The user may use '$MODULE' or '${MODULE}' in the "prefix" 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('prefix'); if (!$path) { return $self->getOption('kdedir'); } my $moduleName = $self->name(); $path =~ s/(\$\{MODULE})|(\$MODULE\b)/$moduleName/g; return $path; } 1;