#!/usr/bin/perl -w =head1 NAME B - auto-correct CMake TARGET_LINK_LIBRARIES directive according to the information provided by GNU ld linker S> errors. =head1 SYNOPSIS B [B<--invoke-edit|-e>] [B<--build-dir|-b>=I] [B<--build-command|-c>=I] [B<--patch-name|-p>=I] [B<--do-backups>] =head1 DESCRIPTION B is capable of correcting most linking failures caused by S<'undefined references'> linker errors. The script should be executed from the extracted debian source tree with all build dependences installed in the environment. This script is a wrapper around S<`debian/rules build'> or whatever command you specify with B<--build-command> option. B depends on B to incrementally build up a patch of the changes it does. The script assumes that quilt patches are located in debian/patches. The default name of the patch is S> (it can be changed with the [B<--patch-name> option). This patch must have already pushed to the "quilt top" when this script is executed. First of all, the script loads dynamic symbol names of the libraries specified in the @LIBS array. Then it starts building process and keeps monitoring it while looking for the "undefined references" errors from the linker (only GNU ld syntax is supported). Then it tries to match undefined symbol names with the symbols loaded from the libraries in the @LIBS array. If matches are found, it tries to update TARGET_LINK_LIBRARIES command in the respective CMakeLists.txt file adding missing libraries. Finally, the build process is restarted. This loop continues until build completes successfully or the error which B can't handle occurs. In latter case, a user will need to `quilt edit' the respective file manually or add more libs to the @LIBS array. Then the script can be restarted again (or use --invoke-edit option to invoke I and restart build for you automatically). CMake verbose output must be enabled for B to work reliably. B was written to help maintainer resolve build failures of KDE applications which were introduced by the clean up of KDELibs recursive library dependences. =head1 OPTIONS =over 4 =item B<-b> I, B<--build-dir>=I Specify a custom build directory. It must be relative the current (source) directory. Default is as returned by S">. =item B<-c> I, B<--build-command>=I The command which should be run to build the source. If the command is not specific, `debian/rules build' will be executed from the B. However, if you specify the command, it will be from the B. =item B<-i>, B<--exec-in-build-dir> Run build command in the build directory. Default is to run in the source directory. =item B<-p>, B<--patch-name>=I The name of the quilt patch in which all changes made by the script will be stored. Default is I<97_fix_target_link_libraries.diff>. =item B<--do-backups>, B<--backup> Whether to backup CMakeLists.txt as CMakeLists.txt.orig before auto-modifying it. =item B<--invoke-edit>, B<--edit>, B<-e> Invoke `quilt edit' on the respective CMakeLists.txt when undefined references cannot be automatically resolved and restart build process immediately when the editor is closed. =item B<--quilt>=I, B<-q> I Use the specified command to invoke quilt instead of default `QUILT_PATCHES=debian/patches quilt' =back =head1 LICENSE This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. On B systems, the complete text of the GNU GPL v3 can be found in the file F =head1 AUTHORS Written by Modestas Vainius =cut use strict; use Cwd qw(getcwd realpath); use File::Spec; use File::Copy; use Getopt::Long; use Pod::Usage; use FileHandle; use IPC::Open2; #### Please add predefined libraries to load dynamic symbol from here. # new Library(name, cmake_target, condition => 'condition', [path]). If path is not specified, # it's /usr/lib/$name.so. Use '' quotes to avoid escaping $. my @LIBS = ( new Library('QtDBus', '${QT_QTDBUS_LIBRARY}'), new Library('QtNetwork', '${QT_QTNETWORK_LIBRARY}'), new Library('QtXml', '${QT_QTXML_LIBRARY}'), new Library('QtSvg', '${QT_QTSVG_LIBRARY}'), new Library('QtGui', '${QT_QTGUI_LIBRARY}'), # new Library('pthread', '${CMAKE_THREAD_LIBS_INIT}', '/lib/libpthread.so.0'), new Library('X11', '${X11_X11_LIB}', condition => 'X11_FOUND'), new Library('Xext', '${X11_Xext_LIB}', condition => 'X11_Xext_FOUND'), new Library('z', '${ZLIB_LIBRARY}'), new Library('solid', '${KDE4_SOLID_LIBS}'), new Library('kdecore', '${KDE4_KDECORE_LIBS}'), new Library('kdeui', '${KDE4_KDEUI_LIBS}'), new Library('kparts', '${KDE4_KPARTS_LIBS}'), new Library('ktexteditor', '${KDE4_KTEXTEDITOR_LIBS}'), new Library('kio', '${KDE4_KIO_LIBS}'), new Library('kde3support', '${KDE4_KDE3SUPPORT_LIBRARY}'), ); #### Some defaults my $MSG_PREFIX = "--=--"; my $QUILT = "QUILT_PATCHES=debian/patches quilt"; ############### Implementation ############################### sub Library::new { my ($cls, $name, $cmake_target, %other) = @_; my $path = $other{path}; my $condition = (exists $other{condition}) ? $other{condition} : undef; if (!defined $path) { $path = "/usr/lib/lib$name.so"; } return bless( { name => $name, path => $path, cmake_target => $cmake_target, condition => $condition }, $cls); } sub Library::load { my $self = shift; my @symbols; my %symbhash; my @cpp_symbols; if (-r $self->{path}) { open(OBJDUMP, "objdump -T '$self->{path}' |") or die "Unable to run objdump"; while() { # 0000000000021e80 g DF .text 00000000000000f9 Base _XSendClientPrefix if (m/^[0-9a-f]+\s+g\s+DF\s+\.text\s+[0-9a-f]+\s+Base\s+(.*)$/) { my $symbol = $1; if ($symbol =~ m/^_Z/) { # It is C++ symbol. Needs demangling. push @cpp_symbols, $symbol; } else { push @symbols, $symbol; $symbhash{$symbol} = 1; } } } close(OBJDUMP); } if (@cpp_symbols) { open2(*OUT, *IN, "c++filt") or die "Unable to run c++filt"; my $count = 0; for (@cpp_symbols) { print IN "$_\n"; $_ = ; chomp; # Don't care about anything after `(' s/\(.*$//; s/^non-virtual thunk to\s+//; push @symbols, $_; $symbhash{$_} = 1; $count++; } close(IN); close(OUT); print(STDERR "Lost a few C++ symbols (", (scalar(@cpp_symbols) - $count), ") while demangling ", $self->to_string(), "\n") unless ($count == scalar(@cpp_symbols)); } if (@symbols) { $self->{symbols} = \@symbols; $self->{symbhash} = \%symbhash; return scalar(@symbols); } else { $self->{symbols} = []; $self->{symbhash} = {}; return 0; } } sub Library::has_symbol { my ($self, $symbol) = @_; for my $sym (@{$self->{symbols}}) { if ($symbol =~ m/^\Q$sym\E/) { return 1; } } return 0; } sub Library::has_symbol_fast { my ($self, $symbol) = @_; # Don't care about anything after `(' $symbol =~ s/\(.*$//; return (exists $self->{symbhash}{$symbol}); } sub Library::to_string() { my ($self) = @_; return $self->{name} . " ( " . $self->{path} . " )"; } sub IgnoreStack::new { return bless( { stack => [] }, shift() ); } sub IgnoreStack::process_line { my ($self, $line) = @_; my $stack = $self->{stack}; if ($line =~ m/^\s*((end)?(foreach|function|if|macro|while))\s*[(]/i) { my $isend = defined $2; my $cmd = uc($3); if ($isend) { if (@$stack) { my $s = pop @$stack; print STDERR "$MSG_PREFIX There is something wrong with IgnoreStack:\n", "$MSG_PREFIX stack top ($s) does not match end command ($cmd)\n" if $s ne $cmd; } else { print STDERR "$MSG_PREFIX IgnoreStack is empty but got 'end$cmd'. A bug probably.\n"; } } else { push @$stack, $cmd; } return 1; # Processed } else { return 0; } } sub IgnoreStack::is_empty { return scalar(@{shift()->{stack}}) == 0; } sub IgnoreStack::dump_stack { my $self = shift; print "Ignore stack dump:\n"; for my $e (@{$self->{stack}}) { print " $e\n"; } } sub determine_needed_libs { my ($alllibs, $undefrefs) = @_; my @_libs; my @libs = (); my @notfound; # Try fast search first nextfastref: for my $ref (@$undefrefs) { my $lib; for my $lib (@$alllibs) { if ($lib->has_symbol_fast($ref)) { push @_libs, $lib; next nextfastref; } } push @notfound, $ref; } # Then try slow one nextslowref: for my $ref (@notfound) { my $lib; for my $lib (@$alllibs) { if ($lib->has_symbol($ref)) { push @_libs, $lib; next nextslowref; } } } # Kill dupes my $prev = ""; for (sort { $a->{cmake_target} cmp $b->{cmake_target} } @_libs) { if ($_ ne $prev) { push @libs, $_; $prev = $_; } } return \@libs; } sub write_target_link_libs { my ($dir, $target, $libs, $do_backups) = @_; my $cmakelists = File::Spec->catfile($dir, "CMakeLists.txt"); my $normlibs = ""; # Unconditional (normal) libs my $condlibs = ""; # Conditional linking my $strlibs = join(" ", map($_->{cmake_target}, @$libs)); # Both for (@$libs) { if (defined $_->{condition}) { $condlibs .= sprintf("if (%s)\n target_link_libraries($target %s)\nendif (%s)\n", $_->{condition}, $_->{cmake_target}, $_->{condition}); } else { $normlibs .= " " if ($normlibs); $normlibs .= $_->{cmake_target}; } } if (-r $cmakelists) { my @contents; my @ignored; my $found = 0; # Ignore directive inside if/endif, while/endwhile etc. blocks my $ignstack = new IgnoreStack; # Read and change open(CMAKELISTS, "<$cmakelists"); while () { if (!$found && !$ignstack->process_line($_) && m/^\s*((?:target_link_libraries|kdepim4_link_unique_libraries)\s*\(\s*$target\s+)(.*?)(\s*\).*)?$/i) { my $newline; if ($normlibs) { # Fix it $newline = $1; my $end = $3; $newline .= $2 if ($2); $newline .= " " if ($newline !~ m/\s+$/); $newline .= $normlibs; $newline .= $end if ($end); $newline .= "\n"; } else { $newline = $_; } $newline .= "\n" . $condlibs . "\n" if ($condlibs); if ($ignstack->is_empty()) { push @contents, $newline; $found = $.; } else { # Save for later use push @ignored, { found => $., newline => $newline }; push @contents, $_; } } else { push @contents, $_; } } close(CMAKELISTS); if (!$ignstack->is_empty()) { $ignstack->dump_stack(); print STDERR "$cmakelists has been processed but ignore stack is not empty. Probably a bug!\n"; } if (!$found && @ignored == 1) { # That's probably it as there were no other candidates. Replace $found = ${ignored[0]}->{found}; my $newline = ${ignored[0]}->{newline}; $contents[$found-1] = $newline; } elsif (!$found && @ignored > 1) { print "$MSG_PREFIX More (", scalar(@ignored), ") than 1 target_link_libraries() found in the IF/WHILE etc. blocks\n"; } if (!$found) { print "$MSG_PREFIX $cmakelists could not be corrected (needed '$strlibs' for target '$target'). Respective target_link_libraries() was not found $MSG_PREFIX\n"; return 0; } else { # Write system("$QUILT add '$cmakelists'"); open(CMAKELISTS, ">$cmakelists.tmp"); for (@contents) { print CMAKELISTS $_; } close(CMAKELISTS); if ($do_backups) { File::Copy::move("$cmakelists", "$cmakelists.orig") or die "Could not rename file '$cmakelists' -> '$cmakelists.org'"; } File::Copy::move("$cmakelists.tmp", "$cmakelists") or die "Could not rename file '$cmakelists.tmp' -> '$cmakelists'"; print "$MSG_PREFIX $cmakelists edited. Added libraries '$strlibs' for target $target\n"; return 1; } } else { die "$cmakelists for target $target could not be found. Something is wrong"; } } sub invoke_edit { my ($cmakelists, $do_invoke) = @_; my $quilt_cmd = "$QUILT edit '$cmakelists'"; if ($do_invoke) { print "$MSG_PREFIX Press ENTER to edit '$cmakelists' or ^C to cancel ..."; <>; return (system($quilt_cmd) == 0) ? 0 : 2; } else { print "$MSG_PREFIX You probably want to run the command to correct the problem yourself: \n", "$MSG_PREFIX \$ $quilt_cmd\n"; return 2; } } sub build_and_fix { my ($sourcedir, $builddir, $buildcmd, $exec_in_build_dir, $do_backups, $invoke_edit) = @_; my $bdir; my $btarget; my $islderror = 0; my @undefrefs; my $link_cmd = 0; if ($exec_in_build_dir) { chdir $builddir; } else { chdir $sourcedir; } open(MAKE, "$buildcmd 2>&1 |") or die "Unable to run `$buildcmd' in $builddir"; while () { # [ 39%] Building CXX object kwin/kcmkwin/kwindecoration/CMakeFiles print $_; chomp; if ($link_cmd) { #cd /buildd/kdebase-workspace/obj-x86_64-linux-gnu/kwin/kcmkwin/kwindecoration && /usr/bin/cmake -E cmake_link_script CMakeFiles/kcm_kwindecoration.dir/link.txt $MSG_PREFIXverbose=1 if (m#^(?:cd ["']?(.*?)["']? && )?.*/cmake -E cmake_link_script CMakeFiles/(.*?)\.dir/#) { $btarget = $2; $bdir = ($1) ? File::Spec->abs2rel(Cwd::realpath($1), $builddir) : "."; $link_cmd = 0; } else { die "Unrecognized link line"; } } elsif (m/\[\s*\d+\%\] Building (.*) object ["']?(.*?)["']?\s*$/) { $bdir = $2; if ($bdir =~ m#CMakeFiles/(.*?)\.dir/#) { $btarget = $1; } else { die "Could not extract target from $bdir"; } $bdir =~ s#/CMakeFiles/.*##; } elsif (m/^Linking (.*) (shared (module|library)|executable) /) { $link_cmd = 1; } elsif (m/undefined reference to `(.*)'$/) { # undefined reference to `QDBusMessage::createSignal(QString const&, QString const&, QString const&)' push @undefrefs, $1; } elsif (m/collect\d+: ld returned \d+ exit status/) { $islderror = 1; } } close(MAKE); chdir $sourcedir; # Try to correct the error # print $MSG_PREFIX, $islderror, $bdir, $btarget, @undefrefs; if ($islderror && $bdir && $btarget && @undefrefs) { my $libs = determine_needed_libs(\@LIBS, \@undefrefs); if (@$libs) { if (write_target_link_libs($bdir, $btarget, $libs, $do_backups)) { return 0; # again } else { return invoke_edit("$bdir/CMakeLists.txt", $invoke_edit); } } else { my $cmakelists = "$bdir/CMakeLists.txt"; print "$MSG_PREFIX Could not resolve linkage problem automatically. Undefined symbols have not been recognized\n"; print "$MSG_PREFIX Target: $btarget; CMakeLists.txt: $cmakelists", "\n"; return invoke_edit($cmakelists, $invoke_edit); } } else { #print $islderror, ", ", $bdir, ", ", $btarget, ", ", @undefrefs, "\n"; print "$MSG_PREFIX Building completed successfully or unrecognized error\n"; return 1; } } sub load_libraries { my $LIBS = shift; print "$MSG_PREFIX Reading dynamic symbol table of ", scalar(@$LIBS), " shared libraries... ", "\n"; my $count = 0; for my $lib (@$LIBS) { print "$MSG_PREFIX Loading ", $lib->to_string(), " ... "; if ($lib->load()) { print "success\n"; $count++; } else { print "failed (path is not readable or has no symbols)\n"; } } return $count; } sub check_environment { my ($builddir, $buildcmd, $patchname) = @_; print "$MSG_PREFIX Script Version v$main::VERSION $MSG_PREFIX", "\n"; if ($buildcmd =~ m/^debian/) { system("dh_testdir") == 0 or die "$MSG_PREFIX Please run this script from the debianized source tree $MSG_PREFIX\n"; } my $toppatch = `$QUILT top`; chomp $toppatch; if (!defined $toppatch || $toppatch ne $patchname) { die "$MSG_PREFIX Quilt top patch must be named '$patchname' when this script is run. Please either:\n" . "$MSG_PREFIX \$ $QUILT push $patchname\n" . "$MSG_PREFIX \$ $QUILT new $patchname\n"; } } sub get_gnu_build_type { my $buildtype = `dpkg-architecture -qDEB_BUILD_GNU_TYPE`; chomp $buildtype; return $buildtype; } ############## Main loop ############################## $main::VERSION = "0.5.6"; my $sourcedir = Cwd::getcwd(); my $builddir = "obj-" . get_gnu_build_type(); my $exec_in_build_dir = 0; my $buildcmd = "debian/rules build"; my $patchname = "97_fix_target_link_libraries.diff"; my $do_backups = 0; my $show_help = 0; my $invoke_edit = 0; if (GetOptions( "help|h|?" => \$show_help, "build-dir|b=s" => \$builddir, "build-command|c=s" => \$buildcmd, "invoke-edit|edit|e!" => \$invoke_edit, "exec-in-build-dir|i!" => \$exec_in_build_dir, "patch-name|p=s" => \$patchname, "do-backups|backup!" => \$do_backups, "quilt|q=s" => \$QUILT, )) { pod2usage(-exitval => 1, -verbose => 2, -noperldoc => 1) if ($show_help); $builddir = Cwd::realpath(File::Spec->catdir($sourcedir, $builddir)); check_environment($builddir, $buildcmd, $patchname); if (load_libraries(\@LIBS) == 0) { die "Error: No dynamic symbols found in predefined libraries"; } my $ret; while (!($ret = build_and_fix($sourcedir, $builddir, $buildcmd, $exec_in_build_dir, $do_backups, $invoke_edit))) { print "$MSG_PREFIX Linkage problem fixed, rebuilding again\n"; } if ($ret == 1) { print "$MSG_PREFIX If build process is complete, you may want to run the following command to build/refresh the patch\n" . "$MSG_PREFIX \$ $QUILT refresh -p ab --no-timestamps --no-index\n"; if ($do_backups) { print "$MSG_PREFIX Also run the following command to cleanup backup files:\n" . "$MSG_PREFIX \$ find -name 'CMakeLists.txt.orig' -delete\n"; } } exit 0; } else { podusage(-exitval => 2, -verbose => 1); }