You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

2649 lines
89 KiB

package ksb::Application 0.20;
# Class: Application
#
# Contains the application-layer logic (i.e. creating a build context, reading
# options, parsing command-line, etc.)
use strict;
use warnings;
use 5.014;
no if $] >= 5.018, 'warnings', 'experimental::smartmatch';
use ksb::Debug;
use ksb::Util;
use ksb::BuildContext;
use ksb::BuildSystem::QMake;
use ksb::BuildException 0.20;
use ksb::Module;
use ksb::ModuleResolver 0.20;
use ksb::ModuleSet 0.20;
use ksb::ModuleSet::KDEProjects;
use ksb::RecursiveFH;
use ksb::DependencyResolver 0.20;
use ksb::IPC::Pipe 0.20;
use ksb::IPC::Null;
use ksb::Updater::Git;
use ksb::Version qw(scriptVersion);
use List::Util qw(first min);
use File::Basename; # basename, dirname
use File::Glob ':glob';
use POSIX qw(:sys_wait_h _exit :errno_h);
use Getopt::Long qw(GetOptionsFromArray :config gnu_getopt nobundling);
use IO::Handle;
use IO::Select;
### Package-specific variables (not shared outside this file).
my $SCRIPT_VERSION = scriptVersion();
# This is a hash since Perl doesn't have a "in" keyword.
my %ignore_list; # List of packages to refuse to include in the build list.
use constant {
# We use a named remote to make some git commands work that don't accept the
# full path.
KDE_PROJECT_ID => 'kde-projects', # git-repository-base for kde_projects.xml
};
### Package methods
sub new
{
my ($class, @options) = @_;
my $self = bless {
context => ksb::BuildContext->new(),
metadata_module => undef,
run_mode => 'build',
modules => undef,
module_factory => undef, # ref to sub that makes a new Module.
# See generateModuleList
_base_pid => $$, # See finish()
}, $class;
# Default to colorized output if sending to TTY
ksb::Debug::setColorfulOutput(-t STDOUT);
my @moduleList = $self->generateModuleList(@options);
$self->{modules} = \@moduleList;
if (!@moduleList) {
print "No modules to build, exiting.\n";
exit 0;
}
$self->context()->setupOperatingEnvironment(); # i.e. niceness, ulimits, etc.
# After this call, we must run the finish() method
# to cleanly complete process execution.
if (!pretending() && !$self->context()->takeLock())
{
print "$0 is already running!\n";
exit 1; # Don't finish(), it's not our lockfile!!
}
# Install signal handlers to ensure that the lockfile gets closed.
_installSignalHandlers(sub {
note ("Signal received, terminating.");
@main::atexit_subs = (); # Remove their finish, doin' it manually
$self->finish(5);
});
return $self;
}
# Method: _readCommandLineOptionsAndSelectors
#
# Returns a list of module/module-set selectors, selected module/module-set
# options, and global options, based on the command-line arguments passed to
# this function.
#
# This is a package method, should be called as
# $app->_readCommandLineOptionsAndSelectors
#
# Phase:
# initialization - Do not call <finish> from this function.
#
# Parameters:
# cmdlineOptions - hashref to hold parsed modules options to be applied later.
# *Note* this must be done separately, it is not handled by this subroutine.
# Global options will be stored in a hashref at $cmdlineOptions->{global}.
# Module or module-set options will be stored in a hashref at
# $cmdlineOptions->{$moduleName} (it will be necessary to disambiguate
# later in the run whether it is a module set or a single module).
#
# If the global option 'start-program' is set, then the program to start and
# its options will be found in a listref pointed to under the
# 'start-program' option.
#
# selectors - listref to hold the list of module or module-set selectors to
# build, in the order desired by the user. These will just be strings, the
# caller will have to figure out whether the selector is a module or
# module-set, and create any needed objects, and then set the recommended
# options as listed in cmdlineOptions.
#
# ctx - <BuildContext> to hold the global build state.
#
# @options - The remainder of the arguments are treated as command line
# arguments to process.
#
# Returns:
# Nothing. An exception will be raised on failure, or this function may quit
# the program directly (e.g. to handle --help, --usage).
sub _readCommandLineOptionsAndSelectors
{
my $self = shift;
my ($cmdlineOptionsRef, $selectorsRef, $ctx, @options) = @_;
my $phases = $ctx->phases();
my @savedOptions = @options; # Copied for use in debugging.
my $version = "kdesrc-build $SCRIPT_VERSION";
my $author = <<DONE;
$version was written (mostly) by:
Michael Pyne <mpyne\@kde.org>
Many people have contributed code, bugfixes, and documentation.
Please report bugs using the KDE Bugzilla, at https://bugs.kde.org/
DONE
# Getopt::Long will store options in %foundOptions, since that is what we
# pass in. To allow for custom subroutines to handle an option it is
# required that the sub *also* be in %foundOptions... whereupon it will
# promptly be overwritten if we're not careful. Instead we let the custom
# subs save to %auxOptions, and read those in back over it later.
my (%foundOptions, %auxOptions);
%foundOptions = (
version => sub { say $version; exit },
author => sub { say $author; exit },
help => sub { _showHelpMessage(); exit 0 },
install => sub {
$self->{run_mode} = 'install';
$phases->phases('install');
},
uninstall => sub {
$self->{run_mode} = 'uninstall';
$phases->phases('uninstall');
},
'no-src' => sub {
$phases->filterOutPhase('update');
},
'no-install' => sub {
$phases->filterOutPhase('install');
},
'no-tests' => sub {
# The "right thing" to do
$phases->filterOutPhase('test');
# What actually works at this point.
$foundOptions{'run-tests'} = 0;
},
'no-build' => sub {
$phases->filterOutPhase('build');
},
# Mostly equivalent to the above
'src-only' => sub {
$phases->phases('update');
# We have an auto-switching function that we only want to run
# if --src-only was passed to the command line, so we still
# need to set a flag for it.
$foundOptions{'allow-auto-repo-move'} = 1;
},
'build-only' => sub {
$phases->phases('build');
},
'install-only' => sub {
$self->{run_mode} = 'install';
$phases->phases('install');
},
prefix => sub {
my ($optName, $arg) = @_;
$auxOptions{prefix} = $arg;
$foundOptions{kdedir} = $arg; #TODO: Still needed for compat?
$foundOptions{reconfigure} = 1;
},
query => sub {
my (undef, $arg) = @_;
my $validMode = qr/^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/;
die("Invalid query mode $arg")
unless $arg =~ $validMode;
# Add useful aliases
$arg = 'source-dir' if $arg =~ /^src-?dir$/;
$arg = 'build-dir' if $arg =~ /^build-?dir$/;
$arg = 'install-dir' if $arg eq 'prefix';
$self->{run_mode} = 'query';
$auxOptions{query} = $arg;
},
pretend => sub {
# Set pretend mode but also force the build process to run.
$auxOptions{pretend} = 1;
$foundOptions{'build-when-unchanged'} = 1;
},
resume => sub {
$auxOptions{resume} = 1;
$phases->filterOutPhase('update'); # Implied --no-src
$foundOptions{'no-metadata'} = 1; # Implied --no-metadata
},
verbose => sub { $foundOptions{'debug-level'} = ksb::Debug::WHISPER },
quiet => sub { $foundOptions{'debug-level'} = ksb::Debug::NOTE },
'really-quiet' => sub { $foundOptions{'debug-level'} = ksb::Debug::WARNING },
debug => sub {
$foundOptions{'debug-level'} = ksb::Debug::DEBUG;
debug ("Commandline was: ", join(', ', @savedOptions));
},
# Hack to set module options
'set-module-option-value' => sub {
my ($optName, $arg) = @_;
my ($module, $option, $value) = split (',', $arg, 3);
if ($module && $option) {
$cmdlineOptionsRef->{$module} //= { };
$cmdlineOptionsRef->{$module}->{$option} = $value;
}
},
# Getopt::Long doesn't set these up for us even though we specify an
# array. Set them up ourselves.
'start-program' => [ ],
'ignore-modules' => [ ],
# Module selectors, the <> is Getopt::Long shortcut for an
# unrecognized non-option value (i.e. an actual argument)
'<>' => sub {
my $arg = shift;
push @{$selectorsRef}, $arg;
},
);
# Handle any "cmdline-eligible" options not already covered.
my $flagHandler = sub {
my ($optName, $optValue) = @_;
# Assume to set if nothing provided.
$optValue = 1 if (!defined $optValue or $optValue eq '');
$optValue = 0 if lc($optValue) eq 'false';
$optValue = 0 if !$optValue;
$auxOptions{$optName} = $optValue;
};
foreach my $option (keys %ksb::BuildContext::defaultGlobalFlags) {
if (!exists $foundOptions{$option}) {
$foundOptions{$option} = $flagHandler; # A ref to a sub here!
}
}
# Actually read the options.
my $optsSuccess = GetOptionsFromArray(\@options, \%foundOptions,
'version', 'author', 'help', 'disable-snapshots|no-snapshots',
'install', 'uninstall', 'no-src|no-svn', 'no-install', 'no-build',
'no-tests', 'build-when-unchanged|force-build', 'no-metadata',
'verbose|v', 'quiet|quite|q', 'really-quiet', 'debug',
'reconfigure', 'colorful-output|color!', 'async!',
'src-only|svn-only', 'build-only', 'install-only', 'build-system-only',
'rc-file=s', 'prefix=s', 'niceness|nice:10', 'ignore-modules=s{,}',
'print-modules', 'pretend|dry-run|p', 'refresh-build',
'query=s', 'start-program|run=s{,}',
'revision=i', 'resume-from=s', 'resume-after=s',
'rebuild-failures', 'resume', 'stop-on-failure',
'stop-after=s', 'stop-before=s', 'set-module-option-value=s',
'metadata-only', 'include-dependencies',
# Special sub used (see above), but have to tell Getopt::Long to look
# for strings
(map { "$_:s" } (keys %ksb::BuildContext::defaultGlobalFlags)),
# Default handling fine, still have to ask for strings.
(map { "$_:s" } (keys %ksb::BuildContext::defaultGlobalOptions)),
'<>', # Required to read non-option args
);
if (!$optsSuccess) {
croak_runtime("Error reading command-line options.");
}
# To store the values we found, need to strip out the values that are
# subroutines, as those are the ones we created. Alternately, place the
# subs inline as an argument to the appropriate option in the
# GetOptionsFromArray call above, but that's ugly too.
my @readOptionNames = grep {
ref($foundOptions{$_}) ne 'CODE'
} (keys %foundOptions);
# Slice assignment: $left{$key} = $right{$key} foreach $key (@keys), but
# with hashref syntax everywhere.
@{ $cmdlineOptionsRef->{'global'} }{@readOptionNames}
= @foundOptions{@readOptionNames};
@{ $cmdlineOptionsRef->{'global'} }{keys %auxOptions}
= values %auxOptions;
}
# Generates the build context and module list based on the command line options
# and module selectors provided, and sets up the module factory.
#
# After this function is called all module set selectors will have been
# expanded, and we will know if we need to download kde-projects metadata or
# not. Dependency resolution has not occurred.
#
# Returns: List of Modules to build.
sub generateModuleList
{
my $self = shift;
my @argv = @_;
# Note: Don't change the order around unless you're sure of what you're
# doing.
my $ctx = $self->context();
my $cmdlineOptions = { global => { }, };
my $cmdlineGlobalOptions = $cmdlineOptions->{global};
my $deferredOptions = { }; # 'options' blocks
# Process --help, --install, etc. first.
my @selectors;
$self->_readCommandLineOptionsAndSelectors($cmdlineOptions, \@selectors,
$ctx, @argv);
my %ignoredSelectors;
@ignoredSelectors{@{$cmdlineGlobalOptions->{'ignore-modules'}}} = undef;
my @startProgramAndArgs = @{$cmdlineGlobalOptions->{'start-program'}};
delete @{$cmdlineGlobalOptions}{qw/ignore-modules start-program/};
# rc-file needs special handling.
if (exists $cmdlineGlobalOptions->{'rc-file'} && $cmdlineGlobalOptions->{'rc-file'}) {
$ctx->setRcFile($cmdlineGlobalOptions->{'rc-file'});
}
# disable async if only running a single phase.
$cmdlineGlobalOptions->{async} = 0 if (scalar $ctx->phases()->phases() == 1);
my $fh = $ctx->loadRcFile();
$ctx->loadPersistentOptions();
if (exists $cmdlineGlobalOptions->{'resume'}) {
my $moduleList = $ctx->getPersistentOption('global', 'resume-list');
if (!$moduleList) {
error ("b[--resume] specified, but unable to find resume point!");
error ("Perhaps try b[--resume-from] or b[--resume-after]?");
croak_runtime("Invalid --resume flag");
}
unshift @selectors, split(/,\s*/, $moduleList);
}
if (exists $cmdlineGlobalOptions->{'rebuild-failures'}) {
my $moduleList = $ctx->getPersistentOption('global', 'last-failed-module-list');
if (!$moduleList) {
error ("b[y[--rebuild-failures] was specified, but unable to determine");
error ("which modules have previously failed to build.");
croak_runtime("Invalid --rebuild-failures flag");
}
unshift @selectors, split(/,\s*/, $moduleList);
}
# _readConfigurationOptions will add pending global opts to ctx while ensuring
# returned modules/sets have any such options stripped out. It will also add
# module-specific options to any returned modules/sets.
my @optionModulesAndSets = _readConfigurationOptions($ctx, $fh, $deferredOptions);
close $fh;
# Check if we're supposed to drop into an interactive shell instead. If so,
# here's the stop off point.
if (@startProgramAndArgs) {
$ctx->setupEnvironment(); # Read options from set-env
$ctx->commitEnvironmentChanges(); # Apply env options to environment
_executeCommandLineProgram(@startProgramAndArgs); # noreturn
}
# Everything else in cmdlineOptions should be OK to apply directly as a module
# or context option.
$ctx->setOption(%{$cmdlineGlobalOptions});
# Selecting modules or module sets would require having the KDE build
# metadata available. This used to be optional, but now everything needs
# it, so download it unilaterally.
$ctx->setKDEProjectMetadataModuleNeeded();
ksb::Updater::Git::verifyGitConfig();
$self->_downloadKDEProjectMetadata();
# The user might only want metadata to update to allow for a later
# --pretend run, check for that here.
if (exists $cmdlineGlobalOptions->{'metadata-only'}) {
return;
}
# At this point we have our list of candidate modules / module-sets (as read in
# from rc-file). The module sets have not been expanded into modules.
# We also might have cmdline "selectors" to determine which modules or
# module-sets to choose. First let's select module sets, and expand them.
my @globalCmdlineArgs = keys %{$cmdlineGlobalOptions};
my $commandLineModules = scalar @selectors;
my $moduleResolver = ksb::ModuleResolver->new($ctx);
$moduleResolver->setCmdlineOptions($cmdlineOptions);
$moduleResolver->setDeferredOptions($deferredOptions);
$moduleResolver->setInputModulesAndOptions(\@optionModulesAndSets);
$moduleResolver->setIgnoredSelectors(keys %ignoredSelectors);
$self->_defineNewModuleFactory($moduleResolver);
my @modules;
if ($commandLineModules) {
@modules = $moduleResolver->resolveSelectorsIntoModules(@selectors);
ksb::Module->setModuleSource('cmdline');
}
else {
# Build everything in the rc-file, in the order specified.
@modules = $moduleResolver->expandModuleSets(@optionModulesAndSets);
if ($ctx->getOption('kde-languages')) {
@modules = _expandl10nModules($ctx, @modules);
}
ksb::Module->setModuleSource('config');
}
# Check for ignored modules (post-expansion)
@modules = grep { ! exists $ignoredSelectors{$_->name()} } @modules;
# If modules were on the command line then they are effectively forced to
# process unless overridden by command line options as well. If phases
# *were* overridden on the command line, then no update pass is required
# (all modules already have correct phases)
@modules = _updateModulePhases(@modules) unless $commandLineModules;
return @modules;
}
# Causes kde-projects metadata to be downloaded (unless --pretend, --no-src, or
# --no-metadata is in effect, although we'll download even in --pretend if
# nothing is available).
#
# No return value.
sub _downloadKDEProjectMetadata
{
my $self = shift;
my $ctx = $self->context();
my $updateNeeded;
my $metadataModule = $ctx->getKDEProjectMetadataModule();
eval {
my $sourceDir = $metadataModule->getSourceDir();
super_mkdir($sourceDir);
my $updateDesired = !$ctx->getOption('no-metadata') && $ctx->phases()->has('update');
$updateNeeded = (! -e $metadataModule->fullpath('source') . "/dependency-data-common");
my $lastUpdate = $ctx->getPersistentOption('global', 'last-metadata-update') // 0;
my $wasPretending = pretending();
if (!$updateDesired && $updateNeeded && (time - ($lastUpdate)) >= 7200) {
warning (" r[b[*] Skipping build metadata update, but it hasn't been updated recently!");
}
if ($updateNeeded && pretending()) {
warning (" y[b[*] Ignoring y[b[--pretend] option to download required metadata\n" .
" y[b[*] --pretend mode will resume after metadata is available.");
ksb::Debug::setPretending(0);
}
if ($updateDesired && (!pretending() || $updateNeeded)) {
$metadataModule->scm()->updateInternal();
$ctx->setPersistentOption('global', 'last-metadata-update', time);
}
ksb::Debug::setPretending($wasPretending);
};
if ($@) {
if (!$updateNeeded) {
warning (" b[r[*] Unable to download required metadata for build process");
warning (" b[r[*] Will attempt to press onward...");
warning (" b[r[*] Exception message: $@");
}
else {
die;
}
}
}
# Returns a list of Modules in the proper build order according to the
# kde-build-metadata dependency information.
#
# The kde-build-metadata repository must have already been updated, and the
# module factory must be setup. The Modules to reorder must be passed as
# arguments.
sub _resolveModuleDependencies
{
my $self = shift;
my $ctx = $self->context();
my $metadataModule = $ctx->getKDEProjectMetadataModule();
my @modules = @_;
@modules = eval {
my $dependencyResolver = ksb::DependencyResolver->new($self->{module_factory});
my $branchGroup = $ctx->effectiveBranchGroup();
for my $file ('dependency-data-common', "dependency-data-$branchGroup")
{
my $dependencyFile = $metadataModule->fullpath('source') . "/$file";
my $dependencies = pretend_open($dependencyFile)
or die "Unable to open $dependencyFile: $!";
debug (" -- Reading dependencies from $dependencyFile");
$dependencyResolver->readDependencyData($dependencies);
close $dependencies;
}
my @reorderedModules = $dependencyResolver->resolveDependencies(@modules);
return @reorderedModules;
};
if ($@) {
warning (" r[b[*] Problems encountered trying to sort modules into correct order:");
warning (" r[b[*] $@");
warning (" r[b[*] Will attempt to continue.");
}
return @modules;
}
# Runs all update, build, install, etc. phases. Basically this *is* the
# script.
# The metadata module must already have performed its update by this point.
sub runAllModulePhases
{
my $self = shift;
my $ctx = $self->context();
my $metadataModule = $ctx->getKDEProjectMetadataModule();
my @modules = $self->modules();
$ctx->addToIgnoreList($metadataModule->scm()->ignoredModules());
# Remove modules that are explicitly blanked out in their branch-group
# i.e. those modules where they *have* a branch-group, and it's set to
# be empty ("").
my $resolver = $ctx->moduleBranchGroupResolver();
my $branchGroup = $ctx->effectiveBranchGroup();
@modules = grep {
my $branch = $_->isKDEProject()
? $resolver->findModuleBranch($_->fullProjectPath(), $branchGroup)
: 1; # Just a placeholder truthy value
whisper ("Removing ", $_->fullProjectPath(), " due to branch-group") if (defined $branch and !$branch);
(!defined $branch or $branch); # This is the actual test
} (@modules);
@modules = $self->_resolveModuleDependencies(@modules);
# Filter --resume-foo options. This might be a second pass, but that should
# be OK since there's nothing different going on from the first pass (in
# resolveSelectorsIntoModules) in that event.
@modules = _applyModuleFilters($ctx, @modules);
if ($ctx->getOption('print-modules')) {
info (" * Module list", $metadataModule ? " in dependency order" : '');
for my $m (@modules) {
say ((" " x ($m->getOption('#dependency-level', 'module') // 0)), "$m");
}
return 0; # Abort execution early!
}
# Add to global module list now that we've filtered everything.
$ctx->addModule($_) foreach @modules;
my $runMode = $self->runMode();
if ($runMode eq 'query') {
my $queryMode = $ctx->getOption('query', 'module');
# Default to ->getOption as query method.
# $_[0] is short name for first param.
my $query = sub { $_[0]->getOption($queryMode) };
$query = sub { $_[0]->fullpath('source') } if $queryMode eq 'source-dir';
$query = sub { $_[0]->fullpath('build') } if $queryMode eq 'build-dir';
$query = sub { $_[0]->installationPath() } if $queryMode eq 'install-dir';
$query = sub { $_[0]->fullProjectPath() } if $queryMode eq 'project-path';
$query = sub { ($_[0]->scm()->_determinePreferredCheckoutSource())[0] // '' }
if $queryMode eq 'branch';
if (@modules == 1) {
# No leading module name, just the value
say $query->($modules[0]);
}
else {
for my $m (@modules) {
say "$m: ", $query->($m);
}
}
return 0;
}
my $result;
if ($runMode eq 'build')
{
# No packages to install, we're in build mode
# What we're going to do is fork another child to perform the source
# updates while we build. Setup for this first by initializing some
# shared memory.
my $ipc = 0;
my $updateOptsSub = sub {
my ($k, $v) = @_;
$ctx->setPersistentOption($k, $v);
};
if ($ctx->getOption('async'))
{
$ipc = ksb::IPC::Pipe->new();
$ipc->setPersistentOptionHandler($updateOptsSub);
}
if (!$ipc)
{
$ipc = ksb::IPC::Null->new();
$ipc->setPersistentOptionHandler($updateOptsSub);
whisper ("Using no IPC mechanism\n");
note ("\n b[<<< Update Process >>>]\n");
$result = _handle_updates ($ipc, $ctx);
note (" b[<<< Build Process >>>]\n");
$result = _handle_build ($ipc, $ctx) || $result;
}
else
{
$result = _handle_async_build ($ipc, $ctx);
$ipc->outputPendingLoggedMessages() if debugging();
}
}
elsif ($runMode eq 'install')
{
$result = _handle_install ($ctx);
}
elsif ($runMode eq 'uninstall')
{
$result = _handle_uninstall ($ctx);
}
_cleanup_log_directory($ctx) if $ctx->getOption('purge-old-logs');
_output_failed_module_lists($ctx);
# Record all failed modules. Unlike the 'resume-list' option this doesn't
# include any successfully-built modules in between failures.
my $failedModules = join(',', map { "$_" } $ctx->listFailedModules());
if ($failedModules) {
# We don't clear the list of failed modules on success so that
# someone can build one or two modules and still use
# --rebuild-failures
$ctx->setPersistentOption('global', 'last-failed-module-list', $failedModules);
}
# env driver is just the ~/.config/kde-env-*.sh, session driver is that + ~/.xsession
if ($ctx->getOption('install-environment-driver') ||
$ctx->getOption('install-session-driver'))
{
_installCustomSessionDriver($ctx);
}
my $color = 'g[b[';
$color = 'r[b[' if $result;
info ("${color}", $result ? ":-(" : ":-)") unless pretending();
return $result;
}
# Method: finish
#
# Exits the script cleanly, including removing any lock files created.
#
# Parameters:
# ctx - Required; BuildContext to use.
# [exit] - Optional; if passed, is used as the exit code, otherwise 0 is used.
sub finish
{
my $self = shift;
my $ctx = $self->context();
my $exitcode = shift // 0;
if (pretending() || $self->{_base_pid} != $$) {
# Abort early if pretending or if we're not the same process
# that was started by the user (e.g. async mode, forked pipe-opens
exit $exitcode;
}
$ctx->closeLock();
$ctx->storePersistentOptions();
my $logdir = $ctx->getLogDir();
note ("Your logs are saved in y[$logdir]");
exit $exitcode;
}
### Package-internal helper functions.
# Reads a "line" from a file. This line is stripped of comments and extraneous
# whitespace. Also, backslash-continued multiple lines are merged into a single
# line.
#
# First parameter is the reference to the filehandle to read from.
# Returns the text of the line.
sub _readNextLogicalLine
{
my $fileReader = shift;
while($_ = $fileReader->readLine()) {
# Remove trailing newline
chomp;
# Replace \ followed by optional space at EOL and try again.
if(s/\\\s*$//)
{
$_ .= $fileReader->readLine();
redo;
}
s/#.*$//; # Remove comments
next if /^\s*$/; # Skip blank lines
return $_;
}
return undef;
}
# Takes an input line, and extracts it into an option name, and simplified
# value. The value has "false" converted to 0, white space simplified (like in
# Qt), and tildes (~) in what appear to be path-like entries are converted to
# the home directory path.
#
# First parameter is the build context (used for translating option values).
# Second parameter is the line to split.
# Return value is (option-name, option-value)
sub _splitOptionAndValue
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my $input = shift;
my $optionRE = qr/\$\{([a-zA-Z0-9-]+)\}/;
# The option is the first word, followed by the
# flags on the rest of the line. The interpretation
# of the flags is dependant on the option.
my ($option, $value) = ($input =~ /^\s* # Find all spaces
([-\w]+) # First match, alphanumeric, -, and _
# (?: ) means non-capturing group, so (.*) is $value
# So, skip spaces and pick up the rest of the line.
(?:\s+(.*))?$/x);
$value //= '';
# Simplify whitespace.
$value =~ s/\s+$//;
$value =~ s/^\s+//;
$value =~ s/\s+/ /g;
# Check for false keyword and convert it to Perl false.
$value = 0 if lc($value) eq 'false';
# Replace reference to global option with their value.
# The regex basically just matches ${option-name}.
my ($sub_var_name) = ($value =~ $optionRE);
while ($sub_var_name)
{
my $sub_var_value = $ctx->getOption($sub_var_name) || '';
if(!$ctx->hasOption($sub_var_name)) {
warning (" *\n * WARNING: $sub_var_name is not set at line y[$.]\n *"); ## TODO: filename is missing
}
debug ("Substituting \${$sub_var_name} with $sub_var_value");
$value =~ s/\$\{$sub_var_name\}/$sub_var_value/g;
# Replace other references as well. Keep this RE up to date with
# the other one.
($sub_var_name) = ($value =~ $optionRE);
}
# Replace tildes with home directory.
1 while ($value =~ s"(^|:|=)~/"$1$ENV{'HOME'}/");
return ($option, $value);
}
# Ensures that the given ModuleSet has at least a valid repository and
# use-modules setting based on the given BuildContext.
sub _validateModuleSet
{
my ($ctx, $moduleSet) = @_;
my $name = $moduleSet->name() || 'unnamed';
my $rcSources = _getModuleSources($moduleSet);
# re-read option from module set since it may be pre-set
my $selectedRepo = $moduleSet->getOption('repository');
if (!$selectedRepo) {
error (<<EOF);
There was no repository selected for the y[b[$name] module-set declared at
$rcSources
A repository is needed to determine where to download the source code from.
Most will want to use the b[g[kde-projects] repository. See also
https://docs.kde.org/trunk5/en/extragear-utils/kdesrc-build/kde-modules-and-selection.html#module-sets
EOF
die make_exception('Config', 'Missing repository option');
}
my $repoSet = $ctx->getOption('git-repository-base');
if ($selectedRepo ne KDE_PROJECT_ID &&
not exists $repoSet->{$selectedRepo})
{
my $projectID = KDE_PROJECT_ID;
my $moduleSetName = $moduleSet->name();
my $moduleSetId = $moduleSetName ? "module-set ($moduleSetName)"
: "module-set";
error (<<EOF);
There is no repository assigned to y[b[$selectedRepo] when assigning a
$moduleSetId at $rcSources.
These repositories are defined by g[b[git-repository-base] in the global
section of your configuration.
Make sure you spelled your repository name right, but you probably meant
to use the magic b[$projectID] repository for your module-set instead.
EOF
die make_exception('Config', 'Unknown repository base');
}
}
# Reads in the options from the config file and adds them to the option store.
# The first parameter is a BuildContext object to use for creating the returned
# ksb::Module under.
# The second parameter is a reference to the file handle to read from.
# The third parameter is the ksb::OptionsBase to use (module, module-set, ctx,
# etc.)
# For global options, just pass in the BuildContext for this param.
# The fourth parameter is optional, if provided it should be a regexp for the
# terminator to use for the block being parsed in the rc file.
#
# The return value is the ksb::OptionsBase provided, with options set as given in
# the configuration file section being processed.
sub _parseModuleOptions
{
my ($ctx, $fileReader, $module, $endRE) = @_;
assert_isa($ctx, 'ksb::BuildContext');
assert_isa($module, 'ksb::OptionsBase');
my $endWord = $module->isa('ksb::BuildContext') ? 'global' :
$module->isa('ksb::ModuleSet') ? 'module-set' :
$module->isa('ksb::Module') ? 'module' :
'options';
# Just look for an end marker if terminator not provided.
$endRE //= qr/^end[\w\s]*$/;
_markModuleSource($module, $fileReader->currentFilename() . ":$.");
# Read in each option
while (($_ = _readNextLogicalLine($fileReader)) && ($_ !~ $endRE))
{
my $current_file = $fileReader->currentFilename();
# Sanity check, make sure the section is correctly terminated
if(/^(module\b|options\b)/)
{
error ("Invalid configuration file $current_file at line $.\nAdd an 'end $endWord' before " .
"starting a new module.\n");
die make_exception('Config', "Invalid file $current_file");
}
my ($option, $value) = _splitOptionAndValue($ctx, $_);
eval { $module->setOption($option, $value); };
if (my $err = $@) {
if (blessed($err) && $err->isa('ksb::BuildException::Config'))
{
my $msg = "$current_file:$.: " . $err->message();
my $explanation = $err->optionUsageExplanation();
$msg = $msg . "\n" . $explanation if $explanation;
$err->setMessage($msg);
}
die; # re-throw
}
}
return $module;
}
# Marks the given OptionsBase subclass (i.e. Module or ModuleSet) as being
# read in from the given string (filename:line). An OptionsBase can be
# tagged under multiple files.
sub _markModuleSource
{
my ($optionsBase, $configSource) = @_;
my $key = '#defined-at';
my $sourcesRef = $optionsBase->hasOption($key)
? $optionsBase->getOption($key)
: [];
push @$sourcesRef, $configSource;
$optionsBase->setOption($key, $sourcesRef);
}
# Returns rcfile sources for given OptionsBase (comma-separated).
sub _getModuleSources
{
my $optionsBase = shift;
my $key = '#defined-at';
my $sourcesRef = $optionsBase->getOption($key) || [];
return join(', ', @$sourcesRef);
}
# Reads in a "moduleset".
#
# First parameter is the build context.
# Second parameter is the filehandle to the config file to read from.
# Third parameter is the ksb::ModuleSet to use.
#
# Returns the ksb::ModuleSet passed in with read-in options set, which may need
# to be further expanded (see ksb::ModuleSet::convertToModules).
sub _parseModuleSetOptions
{
my ($ctx, $fileReader, $moduleSet) = @_;
$moduleSet = _parseModuleOptions($ctx, $fileReader, $moduleSet, qr/^end\s+module(-?set)?$/);
if ($moduleSet->getOption('repository') eq KDE_PROJECT_ID &&
!$moduleSet->isa('ksb::ModuleSet::KDEProjects'))
{
# Perl-specific note! re-blessing the module set into the right 'class'
# You'd probably have to construct an entirely new object and copy the
# members over in other languages.
bless $moduleSet, 'ksb::ModuleSet::KDEProjects';
}
return $moduleSet;
}
# Function: _readConfigurationOptions
#
# Reads in the settings from the configuration, passed in as an open
# filehandle.
#
# Phase:
# initialization - Do not call <finish> from this function.
#
# Parameters:
# ctx - The <BuildContext> to update based on the configuration read and
# any pending command-line options (see cmdlineGlobalOptions).
#
# filehandle - The I/O object to read from. Must handle _eof_ and _readline_
# methods (e.g. <IO::Handle> subclass).
#
# deferredOptions - An out paramter: a hashref holding the options set by any
# 'options' blocks read in by this function. Each key (identified by the name
# of the 'options' block) will point to a hashref value holding the options to
# apply.
#
# Returns:
# @module - Heterogenous list of <Modules> and <ModuleSets> defined in the
# configuration file. No module sets will have been expanded out (either
# kde-projects or standard sets).
#
# Throws:
# - Config exceptions.
sub _readConfigurationOptions
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my $fh = shift;
my $deferredOptionsRef = shift;
my @module_list;
my $rcfile = $ctx->rcFile();
my ($option, %readModules);
my $fileReader = ksb::RecursiveFH->new($rcfile);
$fileReader->addFile($fh, $rcfile);
# Read in global settings
while ($_ = $fileReader->readLine())
{
s/#.*$//; # Remove comments
s/^\s*//; # Remove leading whitespace
next if (/^\s*$/); # Skip blank lines
# First command in .kdesrc-buildrc should be a global
# options declaration, even if none are defined.
if (not /^global\s*$/)
{
error ("Invalid configuration file: $rcfile.");
error ("Expecting global settings section at b[r[line $.]!");
die make_exception('Config', 'Missing global section');
}
# Now read in each global option.
_parseModuleOptions($ctx, $fileReader, $ctx);
last;
}
my $using_default = 1;
my %seenModules; # NOTE! *not* module-sets, *just* modules.
my %seenModuleSets; # and vice versa -- named sets only though!
my %seenModuleSetItems; # To track option override modules.
# Now read in module settings
while ($_ = $fileReader->readLine())
{
s/#.*$//; # Remove comments
s/^\s*//; # Remove leading whitespace
next if (/^\s*$/); # Skip blank lines
# Get modulename (has dash, dots, slashes, or letters/numbers)
my ($type, $modulename) = /^(options|module)\s+([-\/\.\w]+)\s*$/;
my $newModule;
# 'include' directives can change the current file, so check where we're at
$rcfile = $fileReader->currentFilename();
# Module-set?
if (not $modulename) {
my $moduleSetRE = qr/^module-set\s*([-\/\.\w]+)?\s*$/;
($modulename) = m/$moduleSetRE/;
# modulename may be blank -- use the regex directly to match
if (not /$moduleSetRE/) {
error ("Invalid configuration file $rcfile!");
error ("Expecting a start of module section at r[b[line $.].");
die make_exception('Config', 'Ungrouped/Unknown option');
}
if ($modulename && exists $seenModuleSets{$modulename}) {
error ("Duplicate module-set $modulename at $rcfile:$.");
die make_exception('Config', "Duplicate module-set $modulename defined at $rcfile:$.");
}
if ($modulename && exists $seenModules{$modulename}) {
error ("Name $modulename for module-set at $rcfile:$. is already in use on a module");
die make_exception('Config', "Can't re-use name $modulename for module-set defined at $rcfile:$.");
}
# A moduleset can give us more than one module to add.
$newModule = _parseModuleSetOptions($ctx, $fileReader,
ksb::ModuleSet->new($ctx, $modulename || "<module-set at line $.>"));
# Save 'use-modules' entries so we can see if later module decls
# are overriding/overlaying their options.
my @moduleSetItems = $newModule->moduleNamesToFind();
@seenModuleSetItems{@moduleSetItems} = ($newModule) x scalar @moduleSetItems;
$seenModuleSets{$modulename} = $newModule if $modulename;
}
# Duplicate module entry? (Note, this must be checked before the check
# below for 'options' sets)
elsif (exists $seenModules{$modulename} && $type ne 'options') {
error ("Duplicate module declaration b[r[$modulename] on line $. of $rcfile");
die make_exception('Config', "Duplicate module $modulename declared at $rcfile:$.");
}
# Module/module-set options overrides
elsif ($type eq 'options') {
my $options = _parseModuleOptions($ctx, $fileReader,
ksb::OptionsBase->new());
$deferredOptionsRef->{$modulename} = $options->{options};
next; # Don't add to module list
}
# Must follow 'options' handling
elsif (exists $seenModuleSets{$modulename}) {
error ("Name $modulename for module at $rcfile:$. is already in use on a module-set");
die make_exception('Config', "Can't re-use name $modulename for module defined at $rcfile:$.");
}
else {
$newModule = _parseModuleOptions($ctx, $fileReader,
ksb::Module->new($ctx, $modulename));
$seenModules{$modulename} = $newModule;
}
push @module_list, $newModule;
$using_default = 0;
}
while (my ($name, $moduleSet) = each %seenModuleSets) {
_validateModuleSet($ctx, $moduleSet);
}
# If the user doesn't ask to build any modules, build a default set.
# The good question is what exactly should be built, but oh well.
if ($using_default) {
warning (" b[y[*] There do not seem to be any modules to build in your configuration.");
return ();
}
return @module_list;
}
# Exits out of kdesrc-build, executing the user's preferred shell instead. The
# difference is that the environment variables should be as set in kdesrc-build
# instead of as read from .bashrc and friends.
#
# You should pass in the options to run the program with as a list.
#
# Meant to implement the --run command line option.
sub _executeCommandLineProgram
{
my ($program, @args) = @_;
if (!$program)
{
error ("You need to specify a program with the --run option.");
exit 1; # Can't use finish here.
}
if (($< != $>) && ($> == 0))
{
error ("kdesrc-build will not run a program as root unless you really are root.");
exit 1;
}
debug ("Executing b[r[$program] ", join(' ', @args));
exit 0 if pretending();
exec $program, @args or do {
# If we get to here, that sucks, but don't continue.
error ("Error executing $program: $!");
exit 1;
};
}
# Function: _split_url
#
# Subroutine to split a url into a protocol and host
sub _split_url
{
my $url = shift;
my ($proto, $host) = ($url =~ m|([^:]*)://([^/]*)/|);
return ($proto, $host);
}
# Function: _check_for_ssh_agent
#
# Checks if we are supposed to use ssh agent by examining the environment, and
# if so checks if ssh-agent has a list of identities. If it doesn't, we run
# ssh-add (with no arguments) and inform the user. This can be controlled with
# the disable-agent-check parameter.
#
# Parameters:
# 1. Build context
sub _check_for_ssh_agent
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
# Don't bother with all this if the user isn't even using SSH.
return 1 if pretending();
my @svnServers = grep {
$_->scmType() eq 'svn'
} ($ctx->modulesInPhase('update'));
my @gitServers = grep {
$_->scmType() eq 'git'
} ($ctx->modulesInPhase('update'));
my @sshServers = grep {
my ($proto, $host) = _split_url($_->getOption('svn-server'));
# Check if ssh is explicitly used in the proto, or if the host is the
# developer main svn.
(defined $proto && $proto =~ /ssh/) || (defined $host && $host =~ /^svn\.kde\.org/);
} @svnServers;
push @sshServers, grep {
# Check for git+ssh:// or git@git.kde.org:/path/etc.
my $repo = $_->getOption('repository');
($repo =~ /^git\+ssh:\/\//) || ($repo =~ /^[a-zA-Z0-9_.]+@.*:\//);
} @gitServers;
return 1 if (not @sshServers) or $ctx->getOption('disable-agent-check');
whisper ("\tChecking for SSH Agent") if (scalar @sshServers);
# We're using ssh to download, see if ssh-agent is running.
return 1 unless exists $ENV{'SSH_AGENT_PID'};
my $pid = $ENV{'SSH_AGENT_PID'};
# It's supposed to be running, let's see if there exists the program with
# that pid (this check is linux-specific at the moment).
if (-d "/proc" and not -e "/proc/$pid")
{
warning ("r[ *] SSH Agent is enabled, but y[doesn't seem to be running].");
warning ("Since SSH is used to download from Subversion you may want to see why");
warning ("SSH Agent is not working, or correct the environment variable settings.");
return 0;
}
# The agent is running, but does it have any keys? We can't be more specific
# with this check because we don't know what key is required.
my $noKeys = 0;
filter_program_output(sub { $noKeys ||= /no identities/ }, 'ssh-add', '-l');
if ($noKeys)
{
# Use print so user can't inadvertently keep us quiet about this.
print ksb::Debug::colorize (<<EOF);
b[y[*] SSH Agent does not appear to be managing any keys. This will lead to you
being prompted for every module update for your SSH passphrase. So, we're
running g[ssh-add] for you. Please type your passphrase at the prompt when
requested, (or simply Ctrl-C to abort the script).
EOF
my @commandLine = ('ssh-add');
my $identFile = $ctx->getOption('ssh-identity-file');
push (@commandLine, $identFile) if $identFile;
my $result = system (@commandLine);
if ($result) # Run this code for both death-by-signal and nonzero return
{
my $rcfile = $ctx->rcFile();
print "\nUnable to add SSH identity, aborting.\n";
print "If you don't want kdesrc-build to check in the future,\n";
print ksb::Debug::colorize ("Set the g[disable-agent-check] option to g[true] in your $rcfile.\n\n");
return 0;
}
}
return 1;
}
# Function: _handle_updates
#
# Subroutine to update a list of modules.
#
# Parameters:
# 1. IPC module to pass results to.
# 2. Build Context, which will be used to determine the module update list.
#
# The ipc parameter contains an object that is responsible for communicating
# the status of building the modules. This function must account for every
# module in $ctx's update phase to the ipc object before returning.
#
# Returns 0 on success, non-zero on error.
sub _handle_updates
{
my ($ipc, $ctx) = @_;
my $kdesrc = $ctx->getSourceDir();
my @update_list = $ctx->modulesInPhase('update');
# No reason to print out the text if we're not doing anything.
if (!@update_list)
{
$ipc->sendIPCMessage(ksb::IPC::ALL_UPDATING, "update-list-empty");
$ipc->sendIPCMessage(ksb::IPC::ALL_DONE, "update-list-empty");
return 0;
}
if (not _check_for_ssh_agent($ctx))
{
$ipc->sendIPCMessage(ksb::IPC::ALL_FAILURE, "ssh-failure");
return 1;
}
if (not -e $kdesrc)
{
whisper ("KDE source download directory doesn't exist, creating.\n");
if (not super_mkdir ($kdesrc))
{
error ("Unable to make directory r[$kdesrc]!");
$ipc->sendIPCMessage(ksb::IPC::ALL_FAILURE, "no-source-dir");
return 1;
}
}
# Once at this point, any errors we get should be limited to a module,
# which means we can tell the build thread to start.
$ipc->sendIPCMessage(ksb::IPC::ALL_UPDATING, "starting-updates");
my $hadError = 0;
foreach my $module (@update_list)
{
$ipc->setLoggedModule($module->name());
# Note that this must be in this order to avoid accidentally not
# running ->update() from short-circuiting if an error is noted.
$hadError = !$module->update($ipc, $ctx) || $hadError;
}
$ipc->sendIPCMessage(ksb::IPC::ALL_DONE, "had_errors: $hadError");
return $hadError;
}
# Builds the given module.
#
# Return value is the failure phase, or 0 on success.
sub _buildSingleModule
{
my ($ipc, $ctx, $module, $startTimeRef) = @_;
$ctx->resetEnvironment();
$module->setupEnvironment();
my $fail_count = $module->getPersistentOption('failure-count') // 0;
my ($resultStatus, $message) = $ipc->waitForModule($module);
$ipc->forgetModule($module);
if ($resultStatus eq 'failed') {
error ("\tUnable to update r[$module], build canceled.");
$module->setPersistentOption('failure-count', ++$fail_count);
return 'update';
}
elsif ($resultStatus eq 'success') {
note ("\tSource update complete for g[$module]: $message");
}
# Skip actually building a module if the user has selected to skip
# builds when the source code was not actually updated. But, don't skip
# if we didn't successfully build last time.
elsif ($resultStatus eq 'skipped' &&
!$module->getOption('build-when-unchanged') &&
$fail_count == 0)
{
note ("\tSkipping g[$module], its source code has not changed.");
return 0;
}
elsif ($resultStatus eq 'skipped') {
note ("\tNo changes to g[$module] source, proceeding to build.");
}
$$startTimeRef = time;
if ($module->build())
{
$module->setPersistentOption('last-build-rev', $module->currentScmRevision());
$fail_count = 0;
}
else {
++$fail_count;
}
$module->setPersistentOption('failure-count', 0);
return $fail_count > 0 ? 'build' : 0;
}
# Function: _handle_build
#
# Subroutine to handle the build process.
#
# Parameters:
# 1. IPC object to receive results from.
# 2. Build Context, which is used to determine list of modules to build.
#
# If the packages are not already checked-out and/or updated, this
# subroutine WILL NOT do so for you.
#
# This subroutine assumes that the source directory has already been set up.
# It will create the build directory if it doesn't already exist.
#
# If $builddir/$module/.refresh-me exists, the subroutine will
# completely rebuild the module (as if --refresh-build were passed for that
# module).
#
# Returns 0 for success, non-zero for failure.
sub _handle_build
{
my ($ipc, $ctx) = @_;
my @build_done;
my @modules = $ctx->modulesInPhase('build');
my $result = 0;
# No reason to print building messages if we're not building.
return 0 if scalar @modules == 0;
# Check for absolutely essential programs now.
if (!_checkForEssentialBuildPrograms($ctx) &&
!exists $ENV{KDESRC_BUILD_IGNORE_MISSING_PROGRAMS})
{
error (" r[b[*] Aborting now to save a lot of wasted time.");
error (" y[b[*] export KDESRC_BUILD_IGNORE_MISSING_PROGRAMS=1 and re-run (perhaps with --no-src)");
error (" r[b[*] to continue anyways. If this check was in error please report a bug against");
error (" y[b[*] kdesrc-build at https://bugs.kde.org/");
return 1;
}
# IPC queue should have a message saying whether or not to bother with the
# build.
$ipc->waitForStreamStart();
$ctx->unsetPersistentOption('global', 'resume-list');
my $outfile = pretending() ? '/dev/null'
: $ctx->getLogDir() . '/build-status';
open (STATUS_FILE, '>', $outfile) or do {
error (<<EOF);
Unable to open output status file r[b[$outfile]
You won't be able to use the g[--resume] switch next run.\n";
EOF
$outfile = undef;
};
my $num_modules = scalar @modules;
my $statusViewer = $ctx->statusViewer();
my $i = 1;
$statusViewer->numberModulesTotal(scalar @modules);
while (my $module = shift @modules)
{
my $moduleName = $module->name();
my $moduleSet = $module->moduleSet()->name();
my $modOutput = $moduleName;
if (debugging(ksb::Debug::WHISPER)) {
$modOutput .= " (build system " . $module->buildSystemType() . ")"
}
$moduleSet = " from g[$moduleSet]" if $moduleSet;
note ("Building g[$modOutput]$moduleSet ($i/$num_modules)");
my $start_time = time;
my $failedPhase = _buildSingleModule($ipc, $ctx, $module, \$start_time);
my $elapsed = prettify_seconds(time - $start_time);
if ($failedPhase)
{
# FAILURE
$ctx->markModulePhaseFailed($failedPhase, $module);
print STATUS_FILE "$module: Failed on $failedPhase after $elapsed.\n";
if ($result == 0) {
# No failures yet, mark this as resume point
my $moduleList = join(', ', map { "$_" } ($module, @modules));
$ctx->setPersistentOption('global', 'resume-list', $moduleList);
}
$result = 1;
if ($module->getOption('stop-on-failure'))
{
note ("\n$module didn't build, stopping here.");
return 1; # Error
}
$statusViewer->numberModulesFailed(1 + $statusViewer->numberModulesFailed);
}
else {
# Success
print STATUS_FILE "$module: Succeeded after $elapsed.\n";
push @build_done, $moduleName; # Make it show up as a success
$statusViewer->numberModulesSucceeded(1 + $statusViewer->numberModulesSucceeded);
}
$i++;
}
continue # Happens at the end of each loop and on next
{
print "\n"; # Space things out
}
if ($outfile)
{
close STATUS_FILE;
# Update the symlink in latest to point to this file.
my $logdir = $ctx->getSubdirPath('log-dir');
if (-l "$logdir/latest/build-status") {
safe_unlink("$logdir/latest/build-status");
}
symlink($outfile, "$logdir/latest/build-status");
}
info ("<<< g[PACKAGES SUCCESSFULLY BUILT] >>>") if scalar @build_done > 0;
my $successes = scalar @build_done;
# TODO: l10n
my $mods = $successes == 1 ? 'module' : 'modules';
if (not pretending())
{
# Print out results, and output to a file
my $kdesrc = $ctx->getSourceDir();
open BUILT_LIST, ">$kdesrc/successfully-built";
foreach my $module (@build_done)
{
info ("$module") if $successes <= 10;
print BUILT_LIST "$module\n";
}
close BUILT_LIST;
info ("Built g[$successes] $mods") if $successes > 10;
}
else
{
# Just print out the results
if ($successes <= 10) {
info ('g[', join ("]\ng[", @build_done), ']');
}
else {
info ("Built g[$successes] $mods") if $successes > 10;
}
}
info (' '); # Space out nicely
return $result;
}
# Function: _handle_async_build
#
# This subroutine special-cases the handling of the update and build phases, by
# performing them concurrently (where possible), using forked processes.
#
# Only one thread or process of execution will return from this procedure. Any
# other processes will be forced to exit after running their assigned module
# phase(s).
#
# We also redirect ksb::Debug output messages to be sent to a single process
# for display on the terminal instead of allowing them all to interrupt each
# other.
#
# Parameters:
# 1. IPC Object to use for sending/receiving update/build status. It must be
# an object type that supports IPC concurrency (e.g. IPC::Pipe).
# 2. Build Context to use, from which the module lists will be determined.
#
# Returns 0 on success, non-zero on failure.
sub _handle_async_build
{
# The exact method for async is that two children are forked. One child
# is a source update process. The other child is a monitor process which will
# hold status updates from the update process so that the updates may
# happen without waiting for us to be ready to read.
my ($ipc, $ctx) = @_;
print "\n"; # Space out from metadata messages.
my $result = 0;
my $monitorPid = fork;
if ($monitorPid == 0) {
# child
my $updaterToMonitorIPC = ksb::IPC::Pipe->new();
my $updaterPid = fork;
$SIG{INT} = sub { POSIX::_exit(EINTR); };
if ($updaterPid) {
$0 = 'kdesrc-build-updater';
$updaterToMonitorIPC->setSender();
ksb::Debug::setIPC($updaterToMonitorIPC);
POSIX::_exit (_handle_updates ($updaterToMonitorIPC, $ctx));
}
else {
$0 = 'kdesrc-build-monitor';
$ipc->setSender();
$updaterToMonitorIPC->setReceiver();
$ipc->setLoggedModule('#monitor#'); # This /should/ never be used...
ksb::Debug::setIPC($ipc);
POSIX::_exit (_handle_monitoring ($ipc, $updaterToMonitorIPC));
}
}
else {
# Still the parent, let's do the build.
$ipc->setReceiver();
$result = _handle_build ($ipc, $ctx);
}
$ipc->waitForEnd();
$ipc->close();
# Display a message for updated modules not listed because they were not
# built.
my $unseenModulesRef = $ipc->unacknowledgedModules();
if (%$unseenModulesRef) {
note ("The following modules were updated but not built:");
foreach my $modulename (keys %$unseenModulesRef) {
note ("\t$modulename");
}
}
# It's possible if build fails on first module that git or svn is still
# running. Make them stop too.
if (waitpid ($monitorPid, WNOHANG) == 0) {
kill 'INT', $monitorPid;
# Exit code is in $?.
waitpid ($monitorPid, 0);
$result = 1 if $? != 0;
}
return $result;
}
# Function: _handle_install
#
# Handles the installation process. Simply calls 'make install' in the build
# directory, though there is also provision for cleaning the build directory
# afterwards, or stopping immediately if there is a build failure (normally
# every built module is attempted to be installed).
#
# Parameters:
# 1. Build Context, from which the install list is generated.
#
# Return value is a shell-style success code (0 == success)
sub _handle_install
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my @modules = $ctx->modulesInPhase('install');
@modules = grep { $_->buildSystem()->needsInstalled() } (@modules);
my $result = 0;
for my $module (@modules)
{
$ctx->resetEnvironment();
$result = $module->install() || $result;
if ($result && $module->getOption('stop-on-failure')) {
note ("y[Stopping here].");
return 1; # Error
}
}
return $result;
}
# Function: _handle_uninstall
#
# Handles the uninstal process. Simply calls 'make uninstall' in the build
# directory, while assuming that Qt or CMake actually handles it.
#
# The order of the modules is often significant, and it may work better to
# uninstall modules in reverse order from how they were installed. However this
# code does not automatically reverse the order; modules are uninstalled in the
# order determined by the build context.
#
# This function obeys the 'stop-on-failure' option supported by _handle_install.
#
# Parameters:
# 1. Build Context, from which the uninstall list is generated.
#
# Return value is a shell-style success code (0 == success)
sub _handle_uninstall
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my @modules = $ctx->modulesInPhase('uninstall');
@modules = grep { $_->buildSystem()->needsInstalled() } (@modules);
my $result = 0;
for my $module (@modules)
{
$ctx->resetEnvironment();
$result = $module->uninstall() || $result;
if ($result && $module->getOption('stop-on-failure'))
{
note ("y[Stopping here].");
return 1; # Error
}
}
return $result;
}
# Function: _handle_monitoring
#
# This is the main subroutine for the monitoring process when using IPC::Pipe.
# It reads in all status reports from the source update process and then holds
# on to them. When the build process is ready to read information we send what
# we have. Otherwise we're waiting on the update process to send us something.
#
# This convoluted arrangement is required to allow the source update
# process to go from start to finish without undue interruption on it waiting
# to write out its status to the build process (which is usually busy).
#
# Parameters:
# 1. the IPC object to use to send to build process.
# 2. the IPC object to use to receive from update process.
#
# Returns 0 on success, non-zero on failure.
sub _handle_monitoring
{
my ($ipcToBuild, $ipcFromUpdater) = @_;
my @msgs; # Message queue.
# We will write to the build process and read from the update process.
my $sendFH = $ipcToBuild->{fh} || croak_runtime('??? missing pipe to build proc');
my $recvFH = $ipcFromUpdater->{fh} || croak_runtime('??? missing pipe from monitor');
my $readSelector = IO::Select->new($recvFH);
my $writeSelector = IO::Select->new($sendFH);
# Start the loop. We will be waiting on either read or write ends.
# Whenever select() returns we must check both sets.
while (
my ($readReadyRef, $writeReadyRef) =
IO::Select->select($readSelector, $writeSelector, undef))
{
if (!$readReadyRef && !$writeReadyRef) {
# Some kind of error occurred.
return 1;
}
# Check for source updates first.
if (@{$readReadyRef})
{
undef $@;
my $msg = eval { $ipcFromUpdater->receiveMessage(); };
# undef msg indicates EOF, so check for exception obj specifically
die $@ if $@;
# undef can be returned on EOF as well as error. EOF means the
# other side is presumably done.
if (! defined $msg)
{
$readSelector->remove($recvFH);
last; # Select no longer needed, just output to build.
}
else
{
push @msgs, $msg;
# We may not have been waiting for write handle to be ready if
# we were blocking on an update from updater thread.
$writeSelector->add($sendFH) unless $writeSelector->exists($sendFH);
}
}
# Now check for build updates.
if (@{$writeReadyRef})
{
# If we're here the update is still going. If we have no messages
# to send wait for that first.
if (not @msgs)
{
$writeSelector->remove($sendFH);
}
else
{
# Send the message (if we got one).
if (!$ipcToBuild->sendMessage(shift @msgs))
{
error ("r[mon]: Build process stopped too soon! r[$!]");
return 1;
}
}
}
}
# Send all remaining messages.
while (@msgs)
{
if (!$ipcToBuild->sendMessage(shift @msgs))
{
error ("r[mon]: Build process stopped too soon! r[$!]");
return 1;
}
}
$ipcToBuild->close();
return 0;
}
# Function: _applyModuleFilters
#
# Applies any module-specific filtering that is necessary after reading command
# line and rc-file options. (This is as opposed to phase filters, which leave
# each module as-is but change the phases they operate as part of, this
# function could remove a module entirely from the build).
#
# Used for --resume-{from,after} and --stop-{before,after}, but more could be
# added in theory.
# This subroutine supports --{resume,stop}-* for both modules and module-sets.
#
# Parameters:
# ctx - <BuildContext> in use.
# @modules - List of <Modules> or <ModuleSets> to apply filters on.
#
# Returns:
# list of <Modules> or <ModuleSets> with any inclusion/exclusion filters
# applied. Do not assume this list will be a strict subset of the input list,
# however the order will not change amongst the input modules.
sub _applyModuleFilters
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my @moduleList = @_;
if (!$ctx->getOption('resume-from') && !$ctx->getOption('resume-after') &&
!$ctx->getOption('stop-before') && !$ctx->getOption('stop-after'))
{
debug ("No command-line filter seems to be present.");
return @moduleList;
}
if ($ctx->getOption('resume-from') && $ctx->getOption('resume-after'))
{
# This one's an error.
error (<<EOF);
You specified both r[b[--resume-from] and r[b[--resume-after] but you can only
use one.
EOF
croak_runtime("Both --resume-after and --resume-from specified.");
}
if ($ctx->getOption('stop-before') && $ctx->getOption('stop-after'))
{
# This one's an error.
error (<<EOF);
You specified both r[b[--stop-before] and r[b[--stop-after] but you can only
use one.
EOF
croak_runtime("Both --stop-before and --stop-from specified.");
}
return unless @moduleList; # Empty input?
my $resumePoint = $ctx->getOption('resume-from') ||
$ctx->getOption('resume-after');
my $startIndex = scalar @moduleList;
if ($resumePoint) {
debug ("Looking for $resumePoint for --resume-* option");
# || 0 is a hack to force Boolean context.
my $filterInclusive = $ctx->getOption('resume-from') || 0;
my $found = 0;
for (my $i = 0; $i < scalar @moduleList; $i++) {
my $module = $moduleList[$i];
$found = $module->name() eq $resumePoint;
if ($found) {
$startIndex = $filterInclusive ? $i : $i + 1;
$startIndex = min($startIndex, scalar @moduleList - 1);
last;
}
}
}
else {
$startIndex = 0;
}
my $stopPoint = $ctx->getOption('stop-before') ||
$ctx->getOption('stop-after');
my $stopIndex = 0;
if ($stopPoint) {
debug ("Looking for $stopPoint for --stop-* option");
# || 0 is a hack to force Boolean context.
my $filterInclusive = $ctx->getOption('stop-before') || 0;
my $found = 0;
for (my $i = $startIndex; $i < scalar @moduleList; $i++) {
my $module = $moduleList[$i];
$found = $module->name() eq $stopPoint;
if ($found) {
$stopIndex = $i - ($filterInclusive ? 1 : 0);
last;
}
}
}
else {
$stopIndex = scalar @moduleList - 1;
}
if ($startIndex > $stopIndex || scalar @moduleList == 0) {
# Lost all modules somehow.
croak_runtime("Unknown resume -> stop point $resumePoint -> $stopPoint.");
}
return @moduleList[$startIndex .. $stopIndex];
}
# This defines the factory function needed for lower-level code to properly be
# able to create ksb::Module objects from just the module name, while still
# having the options be properly set and having the module properly tied into a
# context.
sub _defineNewModuleFactory
{
my ($self, $resolver) = @_;
my $ctx = $self->context();
$self->{module_factory} = sub {
# We used to need a special module-set to ignore virtual deps (they
# would throw errors if the name did not exist). But, the resolver
# handles that fine as well.
return $resolver->resolveModuleIfPresent(shift);
};
}
# This function converts any 'l10n' references on the command line to return a l10n
# module with the proper build system, scm type, etc.
#
# The languages are selected using global/kde-languages (which should be used
# exclusively from the configuration file).
sub _expandl10nModules
{
my ($ctx, @modules) = @_;
my $l10n = 'l10n-kde4';
assert_isa($ctx, 'ksb::BuildContext');
# Only filter if 'l10n' is actually present in list.
my @matches = grep {$_->name() =~ /^(?:$l10n|l10n)$/} @modules;
my @langs = split(' ', $ctx->getOption('kde-languages'));
return @modules if (!@matches || !@langs);
my $l10nModule;
for my $match (@matches)
{
# Remove all instances of l10n.
@modules = grep {$_->name() ne $match->name()} @modules;
# Save l10n module if user had it in config. We only save the first
# one encountered though.
$l10nModule //= $match;
}
# No l10n module? Just create one.
$l10nModule //= ksb::Module->new($ctx, $l10n);
whisper ("\tAdding languages ", join(';', @langs), " to build.");
$l10nModule->setScmType('l10n');
my $scm = $l10nModule->scm();
# Add all required directories to the l10n module. Its buildsystem should
# know to skip scripts and templates.
$scm->setLanguageDirs(qw/scripts templates/, @langs);
$l10nModule->setBuildSystem($scm);
push @modules, $l10nModule;
return @modules;
}
# Updates the built-in phase list for all Modules passed into this function in
# accordance with the options set by the user.
sub _updateModulePhases
{
whisper ("Filtering out module phases.");
for my $module (@_) {
if ($module->getOption('manual-update') ||
$module->getOption('no-svn') || $module->getOption('no-src'))
{
$module->phases()->clear();
next;
}
if ($module->getOption('manual-build')) {
$module->phases()->filterOutPhase('build');
$module->phases()->filterOutPhase('test');
$module->phases()->filterOutPhase('install');
}
$module->phases()->filterOutPhase('install') unless $module->getOption('install-after-build');
$module->phases()->addPhase('test') if $module->getOption('run-tests');
}
return @_;
}
# This subroutine extract the value from options of the form --option=value,
# which can also be expressed as --option value.
#
# The first parameter is the option that the user passed to the cmd line (e.g.
# --prefix=/opt/foo).
# The second parameter is a reference to the list of command line options.
#
# The return value is the value of the option (the list of options might be
# shorter by 1, copy it if you don't want it to change), or undef if no value
# was provided.
sub _extractOptionValue
{
my ($option, $options_ref) = @_;
if ($option =~ /=/)
{
my @value = split(/=/, $option);
shift @value; # We don't need the first one, that the --option part.
return if (scalar @value == 0);
# If we have more than one element left in @value it's because the
# option itself has an = in it, make sure it goes back in the answer.
return join('=', @value);
}
return if scalar @{$options_ref} == 0;
return shift @{$options_ref};
}
# Like _extractOptionValue, but throws an exception if the value is not
# actually present, so you don't have to check for it yourself. If you do get a
# return value, it will be defined to something.
sub _extractOptionValueRequired
{
my ($option, $options_ref) = @_;
my $returnValue = _extractOptionValue($option, $options_ref);
if (not defined $returnValue) {
croak_runtime("Option $option needs to be set to some value instead of left blank");
}
return $returnValue;
}
# Function: _cleanup_log_directory
#
# This function removes log directories from old kdesrc-build runs. All log
# directories not referenced by $log_dir/latest somehow are made to go away.
#
# Parameters:
# 1. Build context.
#
# No return value.
sub _cleanup_log_directory
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my $logdir = $ctx->getSubdirPath('log-dir');
return 0 if ! -e "$logdir/latest"; # Could happen for error on first run...
# This glob relies on the date being in the specific format YYYY-MM-DD-ID
my @dirs = bsd_glob("$logdir/????-??-??-??/", GLOB_NOSORT);
my @needed = _reachableModuleLogs("$logdir/latest");
# Convert a list to a hash lookup since Perl lacks a "list-has"
my %needed_table;
@needed_table{@needed} = (1) x @needed;
my $length = scalar @dirs - scalar @needed;
if ($length > 15) { # Arbitrary man is arbitrary
note ("Removing y[b[$length] out of g[b[$#dirs] old log directories (this may take some time)...");
}
elsif ($length > 0) {
info ("Removing g[b[$length] out of g[b[$#dirs] old log directories...");
}
for my $dir (@dirs) {
my ($id) = ($dir =~ m/(\d\d\d\d-\d\d-\d\d-\d\d)/);
safe_rmtree($dir) unless $needed_table{$id};
}
}
# Function: _output_failed_module_list
#
# Print out an error message, and a list of modules that match that error
# message. It will also display the log file name if one can be determined.
# The message will be displayed all in uppercase, with PACKAGES prepended, so
# all you have to do is give a descriptive message of what this list of
# packages failed at doing.
#
# No message is printed out if the list of failed modules is empty, so this
# function can be called unconditionally.
#
# Parameters:
# 1. Build Context
# 2. Message to print (e.g. 'failed to foo')
# 3. List of ksb::Modules that had failed to foo
#
# No return value.
sub _output_failed_module_list
{
my ($ctx, $message, @fail_list) = @_;
assert_isa($ctx, 'ksb::BuildContext');
$message = uc $message; # Be annoying
if (@fail_list)
{
debug ("Message is $message");
debug ("\tfor ", join(', ', @fail_list));
}
if (scalar @fail_list > 0)
{
my $homedir = $ENV{'HOME'};
my $logfile;
warning ("\nr[b[<<< PACKAGES $message >>>]");
for my $module (@fail_list)
{
$logfile = $module->getOption('#error-log-file');
# async updates may cause us not to have a error log file stored. There's only
# one place it should be though, take advantage of side-effect of log_command()
# to find it.
if (not $logfile) {
my $logdir = $module->getLogDir() . "/error.log";
$logfile = $logdir if -e $logdir;
}
$logfile = "No log file" unless $logfile;
$logfile =~ s|$homedir|~|;
warning ("r[$module]") if pretending();
warning ("r[$module] - g[$logfile]") if not pretending();
}
}
}
# Function: _output_failed_module_lists
#
# This subroutine reads the list of failed modules for each phase in the build
# context and calls _output_failed_module_list for all the module failures.
#
# Parameters:
# 1. Build context
#
# Return value:
# None
sub _output_failed_module_lists
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
# This list should correspond to the possible phase names (although
# it doesn't yet since the old code didn't, TODO)
for my $phase ($ctx->phases()->phases())
{
my @failures = $ctx->failedModulesInPhase($phase);
_output_failed_module_list($ctx, "failed to $phase", @failures);
}
# See if any modules fail continuously and warn specifically for them.
my @super_fail = grep {
($_->getPersistentOption('failure-count') // 0) > 3
} (@{$ctx->moduleList()});
if (@super_fail)
{
warning ("\nThe following modules have failed to build 3 or more times in a row:");
warning ("\tr[b[$_]") foreach @super_fail;
warning ("\nThere is probably a local error causing this kind of consistent failure, it");
warning ("is recommended to verify no issues on the system.\n");
}
}
# Function: _installTemplatedFile
#
# This function takes a given file and a build context, and installs it to a
# given location while expanding out template entries within the source file.
#
# The template language is *extremely* simple: <% foo %> is replaced entirely
# with the result of $ctx->getOption(foo, 'no-inherit'). If the result
# evaluates false for any reason than an exception is thrown. No quoting of
# any sort is used in the result, and there is no way to prevent expansion of
# something that resembles the template format.
#
# Multiple template entries on a line will be replaced.
#
# The destination file will be created if it does not exist. If the file
# already exists then an exception will be thrown.
#
# Error handling: Any errors will result in an exception being thrown.
#
# Parameters:
# 1. Pathname to the source file (use absolute paths)
# 2. Pathname to the destination file (use absolute paths)
# 3. Build context to use for looking up template values
#
# Return value: There is no return value.
sub _installTemplatedFile
{
my ($sourcePath, $destinationPath, $ctx) = @_;
assert_isa($ctx, 'ksb::BuildContext');
open (my $input, '<', $sourcePath) or
croak_runtime("Unable to open template source $sourcePath: $!");
open (my $output, '>', $destinationPath) or
croak_runtime("Unable to open template output $destinationPath: $!");
while (!eof ($input)) {
my $line = readline($input);
if (!defined ($line)) {
croak_runtime("Failed to read from $sourcePath at line $.: $!");
unlink($destinationPath);
}
# Some lines should only be present in the source as they aid with testing.
next if $line =~ /kdesrc-build: filter/;
$line =~
s {
<% \s* # Template bracket and whitespace
([^\s%]+) # Capture variable name
\s*%> # remaining whitespace and closing bracket
}
{
$ctx->getOption($1, 'module') ||
croak_runtime("Invalid variable $1")
}gxe;
# Replace all matching expressions, use extended regexp w/
# comments, and replacement is Perl code to execute.
(print $output $line) or
croak_runtime("Unable to write line to $destinationPath at line $.: $!");
}
}
# Function: _installCustomFile
#
# This function installs a source file to a destination path, assuming the
# source file is a "templated" source file (see also _installTemplatedFile), and
# records a digest of the file actually installed. This function will overwrite
# a destination if the destination is identical to the last-installed file.
#
# Error handling: Any errors will result in an exception being thrown.
#
# Parameters:
# 1. Build context to use for looking up template values,
# 2. The full path to the source file.
# 3. The full path to the destination file (incl. name)
# 4. The key name to use for searching/recording installed MD5 digest.
#
# Return value: There is no return value.
sub _installCustomFile
{
use File::Copy qw(copy);
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my ($sourceFilePath, $destFilePath, $md5KeyName) = @_;
my $baseName = basename($sourceFilePath);
if (-e $destFilePath) {
my $existingMD5 = $ctx->getPersistentOption('/digests', $md5KeyName) // '';
if (fileDigestMD5($destFilePath) ne $existingMD5) {
if (!$ctx->getOption('#delete-my-settings')) {
error ("\tr[*] Installing \"b[$baseName]\" would overwrite an existing file:");
error ("\tr[*] y[b[$destFilePath]");
error ("\tr[*] If this is acceptable, please delete the existing file and re-run,");
error ("\tr[*] or pass b[--delete-my-settings] and re-run.");
return;
}
elsif (!pretending()) {
copy ($destFilePath, "$destFilePath.kdesrc-build-backup");
}
}
}
if (!pretending()) {
_installTemplatedFile($sourceFilePath, $destFilePath, $ctx);
$ctx->setPersistentOption('/digests', $md5KeyName, fileDigestMD5($destFilePath));
}
}
# Function: _installCustomSessionDriver
#
# This function installs the included sample .xsession and environment variable
# setup files, and records the md5sum of the installed results.
#
# If a file already exists, then its md5sum is taken and if the same as what
# was previously installed, is overwritten. If not the same, the original file
# is left in place and the .xsession is instead installed to
# .xsession-kdesrc-build
#
# Error handling: Any errors will result in an exception being thrown.
#
# Parameters:
# 1. Build context to use for looking up template values,
#
# Return value: There is no return value.
sub _installCustomSessionDriver
{
use FindBin qw($RealBin);
use List::Util qw(first);
use File::Copy qw(copy);
my $ctx = assert_isa(shift, 'ksb::BuildContext');
my @xdgDataDirs = split(':', $ENV{XDG_DATA_DIRS} || '/usr/local/share/:/usr/share/');
my $xdgDataHome = $ENV{XDG_DATA_HOME} || "$ENV{HOME}/.local/share";
# First we have to find the source
my @searchPaths = ($RealBin, map { "$_/apps/kdesrc-build" } ($xdgDataHome, @xdgDataDirs));
s{/+$}{} foreach @searchPaths; # Remove trailing slashes
s{//+}{/}g foreach @searchPaths; # Remove duplicate slashes
my $envScript = first { -f $_ } (
map { "$_/sample-kde-env-master.sh" } @searchPaths
);
my $sessionScript = first { -f $_ } (
map { "$_/sample-xsession.sh" } @searchPaths
);
if (!$envScript || !$sessionScript) {
warning ("b[*] Unable to find helper files to setup a login session.");
warning ("b[*] You will have to setup login yourself, or install kdesrc-build properly.");
return;
}
my $destDir = $ENV{XDG_CONFIG_HOME} || "$ENV{HOME}/.config";
super_mkdir($destDir) unless -d $destDir;
_installCustomFile($ctx, $envScript, "$destDir/kde-env-master.sh",
'kde-env-master-digest');
_installCustomFile($ctx, $sessionScript, "$ENV{HOME}/.xsession",
'xsession-digest') if $ctx->getOption('install-session-driver');
if (!pretending()) {
if ($ctx->getOption('install-session-driver') && !chmod (0744, "$ENV{HOME}/.xsession")) {
error ("\tb[r[*] Error making b[~/.xsession] executable: $!");
error ("\tb[r[*] If this file is not executable you may not be able to login!");
};
}
}
# Function: _checkForEssentialBuildPrograms
#
# This subroutine checks for programs which are absolutely essential to the
# *build* process and returns false if they are not all present. Right now this
# just means qmake and cmake (although this depends on what modules are
# actually present in the build context).
#
# Parameters:
# 1. Build context
#
# Return value:
# None
sub _checkForEssentialBuildPrograms
{
my $ctx = assert_isa(shift, 'ksb::BuildContext');
return 1 if pretending();
my @buildModules = $ctx->modulesInPhase('build');
my %requiredPrograms;
my %modulesRequiringProgram;
foreach my $module ($ctx->modulesInPhase('build')) {
my @progs = $module->buildSystem()->requiredPrograms();
# Deliberately used @, since requiredPrograms can return a list.
@requiredPrograms{@progs} = 1;
foreach my $prog (@progs) {
$modulesRequiringProgram{$prog} //= { };
$modulesRequiringProgram{$prog}->{$module->name()} = 1;
}
}
my $wasError = 0;
for my $prog (keys %requiredPrograms) {
my %requiredPackages = (
qmake => 'Qt',
cmake => 'CMake',
);
my $programPath = absPathToExecutable($prog);
# qmake is not necessarily named 'qmake'
if (!$programPath && $prog eq 'qmake') {
$programPath = ksb::BuildSystem::QMake::absPathToQMake();
}
if (!$programPath) {
# Don't complain about Qt if we're building it...
if ($prog eq 'qmake' && (
grep { $_->buildSystemType() eq 'Qt' } (@buildModules)) ||
pretending()
)
{
next;
}
$wasError = 1;
my $reqPackage = $requiredPackages{$prog} || $prog;
my @modulesNeeding = keys %{$modulesRequiringProgram{$prog}};
local $, = ', '; # List separator in output
error (<<"EOF");
Unable to find r[b[$prog]. This program is absolutely essential for building
the modules: y[@modulesNeeding].
Please ensure the development packages for
$reqPackage are installed by using your distribution's package manager.
You can also see the
https://techbase.kde.org/Getting_Started/Build/Distributions page for
information specific to your distribution (although watch for outdated
information :( ).
EOF
}
}
return !$wasError;
}
# Function: _reachableModuleLogs
#
# Returns a list of module directory IDs that must be kept due to being
# referenced from the "latest" symlink.
#
# This function may call itself recursively if needed.
#
# Parameters:
# 1. The log directory under which to search for symlinks, including the "/latest"
# part of the path.
sub _reachableModuleLogs
{
my $logdir = shift;
my @dirs;
# A lexicalized var (my $foo) is required in face of recursiveness.
opendir(my $fh, $logdir) or croak_runtime("Can't opendir $logdir: $!");
my $dir = readdir($fh);
while(defined $dir) {
if (-l "$logdir/$dir") {
my $link = readlink("$logdir/$dir");
push @dirs, $link;
}
elsif ($dir !~ /^\.{1,2}$/) {
# Skip . and .. directories (this is a great idea, trust me)
push @dirs, _reachableModuleLogs("$logdir/$dir");
}
$dir = readdir $fh;
}
closedir $fh;
# Extract numeric IDs from directory names.
@dirs = map { m/(\d{4}-\d\d-\d\d-\d\d)/ } (@dirs);
# Convert to unique list by abusing hash keys.
my %tempHash;
@tempHash{@dirs} = ();
return keys %tempHash;
}
# Installs the given subroutine as a signal handler for a set of signals which
# could kill the program.
#
# First parameter is a reference to the sub to act as the handler.
sub _installSignalHandlers
{
my $handlerRef = shift;
my @signals = qw/HUP INT QUIT ABRT TERM PIPE/;
@SIG{@signals} = ($handlerRef) x scalar @signals;
}
# Shows a help message and version. Does not exit.
sub _showHelpMessage
{
print <<DONE;
kdesrc-build $SCRIPT_VERSION
https://kdesrc-build.kde.org/
This script automates the download, build, and install process for KDE software
using the latest available source code.
You should first setup a configuration file (~/.kdesrc-buildrc). You can do
this by running the kdesrc-build-setup program, which should be included with
this one. You can also copy the kdesrc-buildrc-sample file (which should be
included) to ~/.kdesrc-buildrc.
Basic synopsis, after setting up .kdesrc-buildrc:
\$ $0 [--options] [module names]
The module names can be either the name of an individual module (as set in your
configuration with a module declaration, or a use-modules declaration), or of a
module set (as set with a module-set declaration).
If you don\'t specify any particular module names, then every module you have
listed in your configuration will be built, in the order listed.
Copyright (c) 2003 - 2015 Michael Pyne <mpyne\@kde.org>, and others.
The script is distributed under the terms of the GNU General Public License
v2, and includes ABSOLUTELY NO WARRANTY!!!
Options:
--no-src Skip contacting the source server.
--no-build Skip the build process.
--no-install Don't automatically install after build.
--pretend Don't actually take major actions, instead describe
what would be done.
--src-only Only update the source code (Identical to --no-build
at this point).
--build-only Build only, don't perform updates or install.
--install-only Only install the already compiled code, this is equivalent
--install to make install/fast in CMake. Useful for example when we
want to clean the install directory but we do not want to
re-compile everything.
--rc-file=<filename> Read configuration from filename instead of default.
--resume-from=<pkg> Skips modules until just before the given package,
then operates as normal.
--resume-after=<pkg> Skips modules up to and including the given package,
then operates as normal.
--stop-before=<pkg> Skips the given package and all later packages.
--stop-after=<pkg> Skips all packages after the given package.
--stop-on-failure Stops the build as soon as a package fails to build.
--reconfigure Run CMake/configure again, but don't clean the build
directory.
--build-system-only Create the build infrastructure, but don't actually
perform the build.
--<option>= Any unrecognized options override an existing global
configuration value, if present.
--set-module-option-value=<module>,<option>,<value>
This option allows you to override an option for a given module, so
that you don't have to change it in the configuration file temporarily.
Use a module name of 'global' for the global configuration.
--pretend (or -p) Don't actually contact the source server, run make,
or create/delete files and directories. Instead,
output what the script would have done.
--refresh-build Start the build from scratch.
--include-dependencies Also try to build known dependencies of the modules
to be built.
--verbose Print verbose output
--help You\'re reading it. :-)
--version Output the program version.
You can get more help by going online to
https://docs.kde.org/trunk5/en/extragear-utils/kdesrc-build/
to view the online documentation.
If you have installed kdesrc-build you may also be able to view the
documentation using KHelpCenter or Konqueror at the URL help:/kdesrc-build, or
using the man page by typing "man kdesrc-build".
The man page can also be found online at
https://kdesrc-build.kde.org/documentation/kdesrc-build.1.html
This help is not comprehensive, to see a listing of all options please visit:
https://docs.kde.org/trunk5/en/extragear-utils/kdesrc-build/conf-options-table.html
For all command line options, please visit:
https://docs.kde.org/trunk5/en/extragear-utils/kdesrc-build/supported-cmdline-params.html
DONE
}
# Accessors
sub context
{
my $self = shift;
return $self->{context};
}
sub metadataModule
{
my $self = shift;
return $self->{metadata_module};
}
sub runMode
{
my $self = shift;
return $self->{run_mode};
}
sub modules
{
my $self = shift;
return @{$self->{modules}};
}
1;