diff --git a/doc/kdesrc-build.desktop b/doc/kdesrc-build.desktop index bc4f683..08295fb 100644 --- a/doc/kdesrc-build.desktop +++ b/doc/kdesrc-build.desktop @@ -1,88 +1,88 @@ # KDE Config File [Desktop Entry] X-DocPath=kdesrc-build/index.html Name=KDE Source Builder Name[bs]=KDE graditelj izvornog koda Name[ca]=Compilació del codi font del KDE Name[ca@valencia]=Compilació del codi font del KDE Name[cs]=Sestavování KDE ze zdrojových kódů Name[da]=KDE Source Builder Name[de]=KDE-Source-Builder Name[el]=Κατασκευαστής πηγής του KDE Name[en_GB]=KDE Source Builder Name[es]=Constructor del código fuente de KDE Name[et]=KDE lähtekoodi ehitaja Name[fi]=KDE-lähdekoodin kääntämisohjelma Name[fr]=Compilateur de sources KDE Name[ga]=Tógálaí Foinse KDE Name[gl]=Compilador do código de KDE Name[hu]=KDE forrásfordító Name[it]=Generazione del sorgente di KDE Name[km]=កម្មវិធី​ស្ថាបនា​ប្រភព​​របស់​ KDE​ Name[ko]=KDE 소스 빌더 Name[lt]=KDE kodo kompiliuoklis Name[mr]=केडीई स्रोतबिल्डर Name[nb]=KDE kildebygger Name[nds]=KDE-Bornkode-Buumoduul Name[nl]=KDE Source Builder Name[pa]=KDE ਸਰੋਤ ਬਿਲਡਰ Name[pl]=Budowanie KDE ze źródeł Name[pt]=Compilação do Código do KDE Name[pt_BR]=Compilação do código do KDE Name[ru]=Сборка KDE из исходного кода Name[sk]=Prekladač zdrojového kódu KDE Name[sl]=Izgrajevalnik izvorne kode za KDE Name[sr]=Градња КДЕ‑а из извора Name[sr@ijekavian]=Градња КДЕ‑а из извора Name[sr@ijekavianlatin]=Gradnja KDE‑a iz izvora Name[sr@latin]=Gradnja KDE‑a iz izvora Name[sv]=KDE-källkodsbyggare Name[tr]=KDE Kaynak Oluşturucu Name[ug]=KDE Source Builder Name[uk]=Програма для збирання KDE з початкових кодів Name[x-test]=xxKDE Source Builderxx Name[zh_CN]=KDE 源代码构建器 Name[zh_TW]=KDE Source Builder Comment=Builds the KDE Platform and associated software from its source code. A command-line only program. Comment[bs]=Gradi platformu KDE‑a i pridruženi softver iz izvornog koda. Program komandne linije. Comment[ca]=Construeix la plataforma del KDE i el programari associat des del seu codi font. Un programa que només és per a la línia d'ordres. -Comment[ca@valencia]=Construeix la plataforma del KDE i el programari associat des del seu codi font. Un programa que només és per a la línia d'ordes. +Comment[ca@valencia]=Construeix la plataforma del KDE i el programari associat des del seu codi font. Un programa que només és per a la línia d'ordres. Comment[cs]=Sestaví ze zdrojových kódů platformu KDE a přidružený software. Nástroj pouze pro příkazovou řádku. Comment[da]=Bygger KDE Platform og tilknyttet software fra kildekode. Et program kun til kommandolinjen. Comment[de]=Erstellt KDE und dazugehörige Software aus den Quelltexten. Es ist ein reines Befehlszeilenprogramm. Comment[el]=Δημιουργεί την πλατφόρμα του KDE και το αντίστοιχο λογισμικό από τον πηγαίο κώδικα. Ένα πρόγραμμα μόνο γραμμής εντολών. Comment[en_GB]=Builds the KDE Platform and associated software from its source code. A command-line only program. Comment[es]=Construye la Plataforma KDE y el software asociado desde su código fuente. Un programa solo para la línea de órdenes. Comment[et]=KDE platvormi ja sellega seotud tarkvara ehitamine lähtekoodist. Ainult käsureal kasutatav programm. Comment[fi]=Kääntää KDE-alustan ja siihen liittyvät ohjelmistot lähdekoodista. On vain komentoriviohjelma. Comment[fr]=Construit la plate-forme KDE et les logiciels associés à partir de son code source. Uniquement un programme en ligne de commandes. Comment[gl]=Compila a plataforma KDE e o software asociado a partires do código fonte. Programa de só liña de ordes. Comment[hu]=Lefordítja a KDE platformot és a kapcsolódó szoftvereket a forráskódjukból. Kizárólag parancssoros program. Comment[it]=Genera la piattaforma di KDE e il software ad essa associato dal codice sorgente. Un programma disponibile solo dalla riga di comando. Comment[km]=ស្ថាបនា​កម្មវិធី​របស់​ KDE និង​កម្មវិធី​​ដែល​ភ្ជាប់​​ពី​កូដ​ប្រភព​របស់​វា​​ ។​ កម្មវិធី​បន្ទាត់​ពាក្យ​បញ្ជា​តែ​​​ប៉ុណ្ណោះ​ ។​ Comment[ko]=KDE 플랫폼과 소프트웨어를 원본 코드에서 빌드합니다. 명령행 전용 프로그램입니다. Comment[lt]=Kompiliuoja KDE platformą ir susijusią programinę įrangą iš išeities kodo. Tik komandinės eilutės programa. Comment[nb]=Bygger KDE-plattformen og tilordnede programmer fra kildekoden. Dette er et program med bare kommandolinje. Comment[nds]=Buut de KDE-Systemümgeven un tohören Programmen ut den Bornkode. Bloots en Konsoolprogramm. Comment[nl]=Bouwt het KDE-platform en geassocieerde software uit zijn broncode. Werkt alleen op de opdrachtregel. Comment[pl]=Budowanie Środowiska KDE i związanego z nim oprogramowania z kodu źródłowego. Program działa tylko w linii poleceń. Comment[pt]=Compila a Plataforma do KDE e os programas associados a partir do seu código-fonte. Um programa apenas para a linha de comandos. Comment[pt_BR]=Compila a Plataforma do KDE e os programas associados a partir do seu código-fonte. Um programa apenas para a linha de comando. Comment[ru]=Собирает платформу KDE и основанное на ней программное обеспечение из исходного кода. Программа предназначена только для запуска из командной строки. Comment[sk]=Vybuduje KDE platformu a asociovaný softvér z jeho zdrojového kódu. Program iba pre príkazový riadok. Comment[sl]=Izgradi okolje KDE in povezane programe iz izvorne kode. Gre za program ukazne vrstice. Comment[sr]=Гради платформу КДЕ‑а и придружени софтвер из изворног кода. Програм командне линије. Comment[sr@ijekavian]=Гради платформу КДЕ‑а и придружени софтвер из изворног кода. Програм командне линије. Comment[sr@ijekavianlatin]=Gradi platformu KDE‑a i pridruženi softver iz izvornog koda. Program komandne linije. Comment[sr@latin]=Gradi platformu KDE‑a i pridruženi softver iz izvornog koda. Program komandne linije. Comment[sv]=Bygger KDE-plattformen och tillhörande programvara från dess källkod. Ett program som bara använder kommandoraden. Comment[tr]=KDE Platformunu ve ilişkili yazılımı kaynak kodundan derler. Sadece komt satırı olan bir uygulama. Comment[ug]=KDE سۇپىسى ۋە مۇناسىۋەتلىك يۇمشاق دېتاللارنى ئەسلى كودىدىن ھاسىل قىلىش. پەقەتلا بۇيرۇق قۇرىدا ئىشلەيدۇ. Comment[uk]=Збирає Платформу KDE і пов’язане з нею програмне забезпечення з початкових кодів. Керується за допомогою командного рядка. Comment[x-test]=xxBuilds the KDE Platform and associated software from its source code. A command-line only program.xx Comment[zh_CN]=从源代码构建 KDE 平台和相关软件。纯命令行程序。 Comment[zh_TW]=從源碼建立 KDE 平台與相關軟體。是一個只有命令列的程式。 Categories=Qt;KDE;Development; Exec=kdialog --sorry "kdesrc-build is a command-line only program. Please read the handbook at help:/kdesrc-build for more information." Terminal=true NoDisplay=true diff --git a/kf5-workspace-build-include b/kf5-workspace-build-include index 9e1b43c..3bfee61 100644 --- a/kf5-workspace-build-include +++ b/kf5-workspace-build-include @@ -1,53 +1,51 @@ # Module definitions for building KDE Workspace 5 # Usage: Write your own kdesrc-buildrc with only a "global" section # (including "branch-group kf5-qt5") # then include this file, like this: # # include extragear/utils/kdesrc-build/kf5-frameworks-build-include # include extragear/utils/kdesrc-build/kf5-applications-build-include # (or using full paths) # # You can then add additional modules if desired. # # This file uses "branch groups" to decide which git branch to use. If you # want to add your application here please be sure to update # kde-build-metadata repo's "logical-module-structure". It includes a simple # tool you can use to validate your change works (or just "kdesrc-build -p # your-module" and look for the right branch). module-set kf5-workspace-modules repository kde-projects # Required for branch-group # Compile everything under kde/workspace use-modules workspace # kdesrc-build can build dependencies (that it knows about) even if you forget # to list them all, if you uncomment this line. # include-dependencies true # Remove if you're somehow using Windows ignore-modules kwindowsaddons # This module doesn't have a buildsystem ignore-modules breeze-grub - # Unmaintained / maintainer doesn't care about build problems - ignore-modules plasma-browser-integration end module-set # Update this module but don't install it, it requires write access to /usr options breeze-plymouth manual-build true end options # For some reason kwalletmanager is in kde/kdeutils, but is considered par of workspace module-set kf5-workspace-utils repository kde-projects use-modules kwalletmanager end module-set module-set kf5-baloo-widgets repository kde-projects # Temporarily here. Not workspace-specific. use-modules baloo-widgets end module-set diff --git a/modules/ksb/BuildSystem/KDE4.pm b/modules/ksb/BuildSystem/KDE4.pm index ac33dcd..a491055 100644 --- a/modules/ksb/BuildSystem/KDE4.pm +++ b/modules/ksb/BuildSystem/KDE4.pm @@ -1,219 +1,225 @@ 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; 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) = @_; $ctx->prependEnvironmentValue('CMAKE_PREFIX_PATH', $prefix); $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_MODULE_PATH', "$qtdir/lib/cmake"); } } sub requiredPrograms { return qw{cmake qmake}; } 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 $result = log_command($module, 'test-results', [ 'make', $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 $target = 'install/fast'; 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")) ], }) == 0; } 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 (_safe_run_cmake ($module)) { error ("\tUnable to configure r[$module] with CMake!"); return 0; } return 1; } ### 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 $module = assert_isa(shift, 'ksb::Module'); my $srcdir = $module->fullpath('source'); my @commands = split_quoted_on_whitespace ($module->getOption('cmake-options')); # grep out empty fields @commands = grep {!/^\s*$/} @commands; # 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 + my $qtdir = $module->getOption('qtdir'); + if ($qtdir && $qtdir ne $prefix) { + 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; # 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]..."); # 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 2e2d5b0..34c4fc9 100644 --- a/modules/ksb/Module.pm +++ b/modules/ksb/Module.pm @@ -1,1109 +1,1112 @@ 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::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::BuildException 0.20; use ksb::BuildSystem 0.30; use ksb::BuildSystem::Autotools; use ksb::BuildSystem::QMake; use ksb::BuildSystem::Qt4; use ksb::BuildSystem::KDE4; use ksb::BuildSystem::CMakeBootstrap; use ksb::ModuleSet::Null; use Mojo::Promise; use Mojo::IOLoop; use POSIX qw(_exit :errno_h); use Storable qw(dclone thaw); use Carp 'confess'; use Scalar::Util 'blessed'; use overload '""' => 'toString', # Add stringify operator. '<=>' => 'compare', ; my $ModuleSource = 'config'; 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, ); # alias our options into the build context for global vis $ctx->{build_options}->{$name} = $self->{options}; @{$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, $moduleSetName) = @_; $self->{'module-set'} = $moduleSetName; } sub setModuleSource { my ($class, $source) = @_; $ModuleSource = $source; } sub moduleSource { my $class = shift; # Should be 'config' or 'cmdline'; return $ModuleSource; } # 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); } 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', 'autotools' => 'ksb::BuildSystem::Autotools', ); 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); } - if (!$buildType && (glob ("$sourceDir/*.pro"))) { + # 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); } # 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 a promise that resolves to true (on success) or rejects with a error # string 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(); return Mojo::Promise->new->reject('There is no build system to use') if ($buildSystem->name() eq 'generic' && !pretending()); # 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'}); my $buildSystemPromise = $self->runPhase_p('buildsystem', sub { return $self->setupBuildSystem(); }, sub { my $was_successful = shift; return Mojo::Promise->new->reject('Unable to setup build system') unless $was_successful; return $was_successful; }); return $buildSystemPromise if $self->getOption('build-system-only'); # If we don't stop with the build system only, then keep extending that # promise chain to complete the build, test, and install return $buildSystemPromise->then(sub { return $self->runPhase_p('build', sub { # called in child process, can block return $buildSystem->buildInternal(); }, sub { # called in this process, with results my $was_successful = shift; $self->setPersistentOption('last-build-rev', $self->currentScmRevision()); return 1 if $was_successful; return Mojo::Promise->new->reject('Build failed'); } ); })->then(sub { return $self->runPhase_p('test', sub { if ($self->getOption('run-tests')) { # TODO: Make test failure a blocker for install? $self->buildSystem()->runTestsuite(); } return 1; }, sub { my $was_successful = shift; return $was_successful; } ); })->then(sub { return $self->runPhase_p('install', sub { if ($self->getOption('install-after-build')) { return 0 if !$self->install(); } else { info ("\tSkipping install for y[$self]"); } return 1; }, sub { my $was_successful = shift; return Mojo::Promise->new->reject unless $was_successful; return $was_successful; } ); }); } # 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 $prefix = $self->installationPath(); # Add global set-envs and context $self->buildContext()->applyUserEnvironment(); my @pkg_config_dirs = ("$kdedir/lib/pkgconfig"); $ctx->prependEnvironmentValue('PKG_CONFIG_PATH', @pkg_config_dirs); my @ld_dirs = ("$kdedir/lib", $self->getOption('libpath')); $ctx->prependEnvironmentValue('LD_LIBRARY_PATH', @ld_dirs); my @path = ("$kdedir/bin", $self->getOption('binpath')); $ctx->prependEnvironmentValue('PATH', @path); # 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. # # In addition it handles the 'latest' symlink to allow for ease of access # to the log directory afterwards. sub getLogDir { my ($self) = @_; return $self->buildContext()->getLogDirFor($self); } sub toString { my $self = shift; return $self->name(); } sub compare { my ($self, $other) = @_; return $self->name() cmp $other->name(); } # returns false if an error, otherwise a string containing information about # number of files/commits/etc affected sub update { my ($self, $ctx) = @_; my $moduleName = $self->name(); my $module_src_dir = $self->getSourceDir(); my $kdesrc = $ctx->getSourceDir(); if ($kdesrc ne $module_src_dir && !super_mkdir($module_src_dir)) { # This module has a different source directory, ensure it exists. error ("Unable to create separate source directory for r[$self]: $module_src_dir"); return 0; } my $fullpath = $self->fullpath('source'); my $count; my $returnValue; eval { $count = $self->scm()->updateInternal() }; # TODO: Just let the exception pass as it has the detail we need # Let calling code worry about updating $ctx if ($@) { if (had_an_exception()) { $ctx->markModulePhaseFailed('build', $self); $@ = $@->{'message'}; } error ("Error updating r[$self], removing from list of packages to build."); error (" > y[$@]"); $self->phases()->filterOutPhase('build'); $returnValue = 0; } else { # TODO: Require updateInternal to set appropriate msg for scm (files, commits, etc.) my $message; if (not defined $count) { $message = ksb::Debug::colorize ("b[y[Unknown changes]."); } elsif ($count) { $message = "1 file affected." if $count == 1; $message = "$count files affected." if $count != 1; } else { $message = "0 files affected."; my $refreshReason = $self->buildSystem()->needsRefreshed(); } $returnValue = $message; } 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'}; } # When running in a subprocess, option changes will be forgotten unless they are fed # back to parent process, so store those in a special array also. if (exists $self->buildContext()->{'#pending'}) { # Only forward 'plain' options, just a sanity check my @keys = grep { !ref($options{$_}) } (keys %options); $self->{options}->{'#pending'} //= { }; @{$self->{options}->{'#pending'}}{@keys} = @options{@keys}; } $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'; # Some global options always override module options. return $ctx->getOption($key) 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) && $ctx->hasOption($key)) { return $ctx->getOption($key) . " " . ($self->{options}{$key} || ''); } # Everything else overrides the global option, unless it's simply not # set at all. return $self->{options}{$key} // $ctx->getOption($key); } # 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). sub fullProjectPath { my $self = shift; my $path = $self->getOption('#xml-full-path', 'module') || croak_internal("Tried to ask for full path of a module $self that doesn't have one!"); return $path; } # 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; } # Runs the given phase in a separate subprocess, using provided sub references. # Assumes use of promises for the provided sub references -- if launching the # subprocess fails, then a rejected promise is returned in the completion sub # reference # Returns a promise that yields the return value of the completion sub # reference. sub runPhase_p { my ($self, $phaseName, $blocking_coderef, $completion_coderef) = @_; my $promise = Mojo::Promise->new; my $ctx = $self->buildContext(); pipe (my $reader, my $writer) or croak_runtime("Couldn't open pipe to subprocess for $phaseName: $!"); # Setup a pipe from child to parent so we can get updates as the phase # progresses, logs, etc. my $reactor = Mojo::IOLoop->singleton->reactor; my $buffer; $reactor->io($reader => sub { my ($reactor) = @_; my $lengthRead = $reader->sysread($buffer, 8192); if ($lengthRead == 0) { # eof $reactor->remove($reader); close $reader; } elsif ($lengthRead > 0) { my $linesRef = eval { thaw($buffer) } || [$@]; $self->buildContext()->statusMonitor()->noteLogEvents( "$self", $phaseName, $linesRef); } else { error("Error reading from pipe: $!"); } }); $reactor->watch($reader, 1, 0); # watch for pipe readability only Mojo::IOLoop->subprocess( sub { # blocks, runs in separate process $SIG{INT} = sub { POSIX::_exit(EINTR); }; $0 = "kdesrc-build[$phaseName]"; # This causes setOption to record changes, and is deliberately not # within ctx->{options} $ctx->{'#pending'} = { }; $writer->autoflush(1); close $reader; # we can't use this anyways ksb::Debug::setOutputHandle($writer); $self->buildContext->resetEnvironment(); $self->setupEnvironment(); # This coderef should return a normal value (something you could # stick in a plain JSON object) my $result = $blocking_coderef->(); my %newOptions; # Grab any newly-set options to feed back to parent my @affectedMods = grep { exists $ctx->{build_options}->{$_}->{'#pending'}; } (keys %{$ctx->{build_options}}); foreach my $affected (@affectedMods) { $newOptions{$affected} = $ctx->{build_options}->{$affected}->{'#pending'}; } return { result => $result, newOptions => \%newOptions, }; }, sub { # runs in this process once subprocess is done my ($subprocess, $err, $resultsRef) = @_; $reactor->remove($reader); close $reader; close $writer; # can't close it earlier because must be open at fork return Mojo::Promise->new->reject($err) if $err; # Apply options that may have changed during child proc execution. if (%{$resultsRef->{newOptions}}) { while(my ($k, $v) = each %{$resultsRef->{newOptions}}) { my %modulesNewOptions = %{$v}; @{$ctx->{build_options}->{$k}}{keys %modulesNewOptions} = values %modulesNewOptions; } } # This coderef should resolve or reject the promise, if used $promise->resolve($completion_coderef->($resultsRef->{result})); } ); return $promise; } 1; diff --git a/modules/ksb/Updater/Git.pm b/modules/ksb/Updater/Git.pm index b62a183..4a92f9a 100644 --- a/modules/ksb/Updater/Git.pm +++ b/modules/ksb/Updater/Git.pm @@ -1,815 +1,812 @@ package ksb::Updater::Git 0.15; # Module which is responsible for updating git-based source code modules. Can # have some features overridden by subclassing (see ksb::Updater::KDEProject # for an example). use strict; use warnings; use 5.014; use parent qw(ksb::Updater); use ksb::Debug; use ksb::Util; use File::Basename; # basename use File::Spec; # tmpdir use POSIX qw(strftime); use List::Util qw(first); use IPC::Cmd qw(run_forked); use constant { DEFAULT_GIT_REMOTE => 'origin', }; # scm-specific update procedure. # May change the current directory as necessary. sub updateInternal { my $self = assert_isa(shift, 'ksb::Updater::Git'); return $self->updateCheckout(); } sub name { return 'git'; } sub currentRevisionInternal { my $self = assert_isa(shift, 'ksb::Updater::Git'); return $self->commit_id('HEAD'); } # Returns the current sha1 of the given git "commit-ish". sub commit_id { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $commit = shift or croak_internal("Must specify git-commit to retrieve id for"); my $module = $self->module(); my $gitdir = $module->fullpath('source') . '/.git'; # Note that the --git-dir must come before the git command itself. my ($id, undef) = filter_program_output( undef, # No filter qw/git --git-dir/, $gitdir, 'rev-parse', $commit, ); chomp $id if $id; return $id; } sub _verifyRefPresent { my ($self, $module, $repo) = @_; my ($commitId, $commitType) = $self->_determinePreferredCheckoutSource($module); return 1 if pretending(); - my $ref = (($commitType eq 'branch') ? 'refs/heads/' - : ($commitType eq 'tag') ? 'refs/tags/' - : '') . $commitId; + my $ref = $commitId; my $hashref = run_forked("git ls-remote --exit-code $repo $ref", { timeout => 10, discard_output => 1, terminate_on_parent_sudden_death => 1}); my $result = $hashref->{exit_code}; return 0 if ($result == 2); # Connection successful, but ref not found return 1 if ($result == 0); # Ref is present croak_runtime("git had error exit $result when verifying $ref present in repository at $repo"); } # Perform a git clone to checkout the latest branch of a given git module # # First parameter is the repository (typically URL) to use. # Throws an exception if it fails. sub _clone { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $git_repo = shift; my $module = $self->module(); my $srcdir = $module->fullpath('source'); my @args = ('--', $git_repo, $srcdir); note ("Cloning g[$module]"); p_chdir($module->getSourceDir()); my ($commitId, $commitType) = $self->_determinePreferredCheckoutSource($module); - $commitId = "refs/tags/$commitId" if $commitType eq 'tag'; + $commitId =~ s,^refs/tags/,,; # git-clone -b doesn't like refs/tags/ unshift @args, '-b', $commitId; # Checkout branch right away if (0 != log_command($module, 'git-clone', ['git', 'clone', @args])) { croak_runtime("Failed to make initial clone of $module"); } #$ipc->notifyPersistentOptionChange($module->name(), 'git-cloned-repository', $git_repo); p_chdir($srcdir); # Setup user configuration if (my $name = $module->getOption('git-user')) { my ($username, $email) = ($name =~ /^([^<]+) +<([^>]+)>$/); if (!$username || !$email) { croak_runtime("Invalid username or email for git-user option: $name". " (should be in format 'User Name '"); } whisper ("\tAdding git identity $name for new git module $module"); my $result = (safe_system(qw(git config --local user.name), $username) >> 8) == 0; $result = (safe_system(qw(git config --local user.email), $email) >> 8 == 0) || $result; if (!$result) { warning ("Unable to set user.name and user.email git config for y[b[$module]!"); } } return; } # Either performs the initial checkout or updates the current git checkout # for git-using modules, as appropriate. # # If errors are encountered, an exception is raised. # # Returns the number of *commits* affected. sub updateCheckout { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $srcdir = $module->fullpath('source'); if (-d "$srcdir/.git") { # Note that this function will throw an exception on failure. return $self->updateExistingClone(); } else { # Check if an existing source directory is there somehow. if (-e "$srcdir" && !is_dir_empty($srcdir)) { if ($module->getOption('#delete-my-patches')) { warning ("\tRemoving conflicting source directory " . "as allowed by --delete-my-patches"); warning ("\tRemoving b[$srcdir]"); safe_rmtree($srcdir) or croak_internal("Unable to delete $srcdir!"); } else { error (<getOption('repository'); if (!$git_repo) { croak_internal("Unable to checkout $module, you must specify a repository to use."); } if (!$self->_verifyRefPresent($module, $git_repo)) { - if (!$self->_moduleIsNeeded()) { - note ("Skipping g[$module], this module was not in the containing module-set at this branch"); - return 0; - } - - croak_runtime("The desired git reference is not available for $module"); + croak_runtime( + $self->_moduleIsNeeded() + ? "$module build was requested, but it has no source code at the requested git branch" + : "The required git branch does not exist at the source repository" + ); } $self->_clone($git_repo); return 1 if pretending(); return count_command_output('git', '--git-dir', "$srcdir/.git", 'ls-files'); } return 0; } # Intended to be reimplemented sub _moduleIsNeeded { return 1; } # Selects a git remote for the user's selected repository (preferring a # defined remote if available, using 'origin' otherwise). # # Assumes the current directory is already set to the source directory. # # Throws an exception on error. # # Return value: Remote name that should be used for further updates. # # See also the 'repository' module option. sub _setupBestRemote { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $cur_repo = $module->getOption('repository'); # Search for an existing remote name first. If none, add our alias. my @remoteNames = $self->bestRemoteName($cur_repo); if (!@remoteNames) { # The desired repo doesn't have a named remote, this should be # because the user switched it in the rc-file. We control the # 'origin' remote to fix this. if ($self->hasRemote(DEFAULT_GIT_REMOTE)) { if (log_command($module, 'git-update-remote', ['git', 'remote', 'set-url', DEFAULT_GIT_REMOTE, $cur_repo]) != 0) { croak_runtime("Unable to update the fetch URL for existing remote alias for $module"); } } elsif (log_command($module, 'git-remote-setup', ['git', 'remote', 'add', DEFAULT_GIT_REMOTE, $cur_repo]) != 0) { croak_runtime("Unable to add a git remote named " . DEFAULT_GIT_REMOTE . " for $cur_repo"); } push @remoteNames, DEFAULT_GIT_REMOTE; } # Make a notice if the repository we're using has moved. my $old_repo = $module->getPersistentOption('git-cloned-repository'); if ($old_repo and ($cur_repo ne $old_repo)) { note (" y[b[*]\ty[$module]'s selected repository has changed"); note (" y[b[*]\tfrom y[$old_repo]"); note (" y[b[*]\tto b[$cur_repo]"); note (" y[b[*]\tThe git remote named b[", DEFAULT_GIT_REMOTE, "] has been updated"); # Update what we think is the current repository on-disk. #$ipc->notifyPersistentOptionChange($module->name(), 'git-cloned-repository', $cur_repo); } return $remoteNames[0]; } # Completes the steps needed to update a git checkout to be checked-out to # a given remote-tracking branch. Any existing local branch with the given # branch set as upstream will be used if one exists, otherwise one will be # created. The given branch will be rebased into the local branch. # # No checkout is done, this should be performed first. # Assumes we're already in the needed source dir. # Assumes we're in a clean working directory (use git-stash to achieve # if necessary). # # First parameter is the remote to use. # Second parameter is the branch to update to. # Returns boolean success flag. # Exception may be thrown if unable to create a local branch. sub _updateToRemoteHead { my $self = shift; my ($remoteName, $branch) = @_; my $module = $self->module(); # The 'branch' option requests a given head in the user's selected # repository. Normally the remote head is mapped to a local branch, # which can have a different name. So, first we make sure the remote # head is actually available, and if it is we compare its SHA1 with # local branches to find a matching SHA1. Any local branches that are # found must also be remote-tracking. If this is all true we just # re-use that branch, otherwise we create our own remote-tracking # branch. my $branchName = $self->getRemoteBranchName($remoteName, $branch); if (!$branchName) { my $newName = $self->makeBranchname($remoteName, $branch); whisper ("\tUpdating g[$module] with new remote-tracking branch y[$newName]"); if (0 != log_command($module, 'git-checkout-branch', ['git', 'checkout', '-b', $newName, "$remoteName/$branch"])) { croak_runtime("Unable to perform a git checkout of $remoteName/$branch to a local branch of $newName"); } } else { whisper ("\tUpdating g[$module] using existing branch g[$branchName]"); if (0 != log_command($module, 'git-checkout-update', ['git', 'checkout', $branchName])) { croak_runtime("Unable to perform a git checkout to existing branch $branchName"); } # On the right branch, merge in changes. return 0 == log_command($module, 'git-rebase', ['git', 'rebase', "$remoteName/$branch"]); } return 1; } # Completes the steps needed to update a git checkout to be checked-out to # a given commit. The local checkout is left in a detached HEAD state, # even if there is a local branch which happens to be pointed to the # desired commit. Based the given commit is used directly, no rebase/merge # is performed. # # No checkout is done, this should be performed first. # Assumes we're already in the needed source dir. # Assumes we're in a clean working directory (use git-stash to achieve # if necessary). # # First parameter is the commit to update to. This can be in pretty # much any format that git itself will respect (e.g. tag, sha1, etc.). # It is recommended to use refs/$foo/$bar syntax for specificity. # Returns boolean success flag. sub _updateToDetachedHead { my ($self, $commit) = @_; my $module = $self->module(); info ("\tDetaching head to b[$commit]"); return 0 == log_command($module, 'git-checkout-commit', ['git', 'checkout', $commit]); } # Updates an already existing git checkout by running git pull. # # Throws an exception on error. # # Return parameter is the number of affected *commits*. sub updateExistingClone { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $cur_repo = $module->getOption('repository'); my $result; p_chdir($module->fullpath('source')); # Try to save the user if they are doing a merge or rebase if (-e '.git/MERGE_HEAD' || -e '.git/rebase-merge' || -e '.git/rebase-apply') { croak_runtime ("Aborting git update for $module, you appear to have a rebase or merge in progress!"); } my $remoteName = $self->_setupBestRemote(); # Download updated objects. This also updates remote heads so do this # before we start comparing branches and such. if (0 != log_command($module, 'git-fetch', ['git', 'fetch', '--tags', $remoteName])) { croak_runtime ("Unable to perform git fetch for $remoteName ($cur_repo)"); } # Now we need to figure out if we should update a branch, or simply # checkout a specific tag/SHA1/etc. my ($commitId, $commitType) = $self->_determinePreferredCheckoutSource($module); note ("Updating g[$module] (to $commitType b[$commitId])"); my $start_commit = $self->commit_id('HEAD'); my $updateSub; if ($commitType eq 'branch') { $updateSub = sub { $self->_updateToRemoteHead($remoteName, $commitId) }; } else { $updateSub = sub { $self->_updateToDetachedHead($commitId); } } # With all remote branches fetched, and the checkout of our desired # branch completed, we can now use our update sub to complete the # changes. $self->stashAndUpdate($updateSub); return count_command_output('git', 'rev-list', "$start_commit..HEAD"); } # Goes through all the various combination of git checkout selection options in # various orders of priority. # # Returns a *list* containing: (the resultant symbolic ref/or SHA1,'branch' or # 'tag' (to determine if something like git-pull would be suitable or whether # you have a detached HEAD)). Since the sym-ref is returned first that should # be what you get in a scalar context, if that's all you want. sub _determinePreferredCheckoutSource { my ($self, $module) = @_; $module //= $self->module(); my @priorityOrderedSources = ( # option-name type getOption-inheritance-flag [qw(commit tag module)], [qw(revision tag module)], [qw(tag tag module)], [qw(branch branch module)], [qw(branch-group branch module)], [qw(use-stable-kde branch module)], # commit/rev/tag don't make sense for git as globals [qw(branch branch allow-inherit)], [qw(branch-group branch allow-inherit)], [qw(use-stable-kde branch allow-inherit)], ); # For modules that are not actually a 'proj' module we skip branch-group # and use-stable-kde entirely to allow for global/module branch selection # options to be selected... kind of complicated, but more DWIMy if (!$module->scm()->isa('ksb::Updater::KDEProject')) { @priorityOrderedSources = grep { $_->[0] ne 'branch-group' && $_->[0] ne 'use-stable-kde' } @priorityOrderedSources; } my $checkoutSource; # Sorry about the !!, easiest way to be clear that bool context is intended my $sourceTypeRef = first { !!($checkoutSource = ($module->getOption($_->[0], $_->[2]) // '')) } @priorityOrderedSources; if (!$sourceTypeRef) { return qw(master branch); } # One fixup is needed for use-stable-kde, to pull the actual branch name # from the right spot. Although if no branch name is set we use master, # without trying to search again. if ($sourceTypeRef->[0] eq 'use-stable-kde') { $checkoutSource = $module->getOption('#branch:stable', 'module') || 'master'; } # Likewise branch-group requires special handling. checkoutSource is # currently the branch-group to be resolved. if ($sourceTypeRef->[0] eq 'branch-group') { assert_isa($self, 'ksb::Updater::KDEProject'); $checkoutSource = $self->_resolveBranchGroup($checkoutSource); if (!$checkoutSource) { my $branchGroup = $module->getOption('branch-group'); whisper ("No specific branch set for $module and $branchGroup, using master!"); $checkoutSource = 'master'; } } if ($sourceTypeRef->[0] eq 'tag' && $checkoutSource !~ m{^refs/tags/}) { $checkoutSource = "refs/tags/$checkoutSource"; } return ($checkoutSource, $sourceTypeRef->[1]); } # Splits a URI up into its component parts. Taken from # http://search.cpan.org/~ether/URI-1.67/lib/URI.pm # Copyright Gisle Aas under the following terms: # "This program is free software; you can redistribute it and/or modify it # under the same terms as Perl itself." sub _splitUri { my($scheme, $authority, $path, $query, $fragment) = $_[0] =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|; return ($scheme, $authority, $path, $query, $fragment); } # This stashes existing changes if necessary, and then runs a provided # update routine in order to advance the given module to the desired head. # Finally, if changes were stashed, they are applied and the stash stack is # popped. # # It is assumed that the required remote has been setup already, that we # are on the right branch, and that we are already in the correct # directory. # # First parameter is a reference to the subroutine to run. This subroutine # should need no parameters and return a boolean success indicator. It may # throw exceptions. # # Throws an exception on error. # # No return value. sub stashAndUpdate { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $updateSub = shift; my $module = $self->module(); my $date = strftime ("%F-%R", gmtime()); # ISO Date, hh:mm time # To find out if we should stash, we just use git diff --quiet, twice to # account for the index and the working dir. # Note: Don't use safe_system, as the error code is stripped to the exit code my $status = pretending() ? 0 : system('git', 'diff', '--quiet'); if ($status == -1 || $status & 127) { croak_runtime("$module doesn't appear to be a git module."); } my $needsStash = 0; if ($status) { # There are local changes. $needsStash = 1; } else { $status = pretending() ? 0 : system('git', 'diff', '--cached', '--quiet'); if ($status == -1 || $status & 127) { croak_runtime("$module doesn't appear to be a git module."); } else { $needsStash = ($status != 0); } } if ($needsStash) { info ("\tLocal changes detected, stashing them away..."); $status = log_command($module, 'git-stash-save', [ qw(git stash save --quiet), "kdesrc-build auto-stash at $date", ]); if ($status != 0) { croak_runtime("Unable to stash local changes for $module, aborting update."); } } if (!$updateSub->()) { error ("\tUnable to update the source code for r[b[$module]"); return; } # Update is performed and successful, re-apply the stashed changes if ($needsStash) { info ("\tModule updated, reapplying your local changes."); $status = log_command($module, 'git-stash-pop', [ qw(git stash pop --index --quiet) ]); if ($status != 0) { error (<module(); my $chosenName; # Use "$branch" directly if not already used, otherwise try to prefix # with the remote name. for my $possibleBranch ($branch, "$remoteName-$branch", "ksdc-$remoteName-$branch") { my $result = system('git', 'show-ref', '--quiet', '--verify', '--', "refs/heads/$possibleBranch") >> 8; return $possibleBranch if $result == 1; } croak_runtime("Unable to find good branch name for $module branch name $branch"); } # Returns the number of lines in the output of the given command. The command # and all required arguments should be passed as a normal list, and the current # directory should already be set as appropriate. # # Return value is the number of lines of output. # Exceptions are raised if the command could not be run. sub count_command_output { # Don't call with $self->, all args are passed to filter_program_output my @args = @_; my $count = 0; filter_program_output(sub { $count++ if $_ }, @args); return $count; } # A simple wrapper that is used to split the output of 'git config --null' # correctly. All parameters are then passed to filter_program_output (so look # there for help on usage). sub slurp_git_config_output { # Don't call with $self->, all args are passed to filter_program_output local $/ = "\000"; # Split on null # This gets rid of the trailing nulls for single-line output. (chomp uses # $/ instead of hardcoding newline chomp(my @output = filter_program_output(undef, @_)); # No filter return @output; } # Returns true if the git module in the current directory has a remote of the # name given by the first parameter. sub hasRemote { my ($self, $remote) = @_; my $hasRemote = 0; eval { filter_program_output(sub { $hasRemote ||= ($_ && /^$remote/) }, 'git', 'remote'); }; return $hasRemote; } # Subroutine to add the 'kde:' alias to the user's git config if it's not # already set. # # Call this as a static class function, not as an object method # (i.e. ksb::Updater::Git::verifyGitConfig, not $foo->verifyGitConfig) # # Returns false on failure of any sort, true otherwise. sub verifyGitConfig { my $configOutput = qx'git config --global --get url.git://anongit.kde.org/.insteadOf kde:'; # 0 means no error, 1 means no such section exists -- which is OK if ((my $errNum = $? >> 8) >= 2) { my $error = "Code $errNum"; my %errors = ( 3 => 'Invalid config file (~/.gitconfig)', 4 => 'Could not write to ~/.gitconfig', 2 => 'No section was provided to git-config', 1 => 'Invalid section or key', 5 => 'Tried to set option that had no (or multiple) values', 6 => 'Invalid regexp with git-config', 128 => 'HOME environment variable is not set (?)', ); $error = $errors{$errNum} if exists $errors{$errNum}; error (" r[*] Unable to run b[git] command:\n\t$error"); return 0; } # If we make it here, I'm just going to assume git works from here on out # on this simple task. if ($configOutput !~ /^kde:\s*$/) { whisper ("\tAdding git download kde: alias"); my $result = safe_system( qw(git config --global --add url.git://anongit.kde.org/.insteadOf kde:) ) >> 8; return 0 if $result != 0; } $configOutput = qx'git config --global --get url.git@git.kde.org:.pushInsteadOf kde:'; if ($configOutput !~ /^kde:\s*$/) { whisper ("\tAdding git upload kde: alias"); my $result = safe_system( qw(git config --global --add url.git@git.kde.org:.pushInsteadOf kde:) ) >> 8; return 0 if $result != 0; } return 1; } 1;