Add experimental dh_gentdeb
authorNeil Williams <neil@codehelp.co.uk>
Fri, 28 Nov 2008 17:17:24 +0000 (18:17 +0100)
committerNeil Williams <neil@codehelp.co.uk>
Fri, 28 Nov 2008 17:17:24 +0000 (18:17 +0100)
debian/changelog
dh_gentdeb [new file with mode: 0755]

index 847c70d..70f1b94 100644 (file)
@@ -1,10 +1,14 @@
 debhelper (7.1.1) UNRELEASED; urgency=low
 
+  [ Joey Hess ]
   * dh_install(1): Order options alphabetically. Closes:# 503896
   * Fix some docs that refered to --srcdir rather than --sourcedir.
     Closes: #504742
 
- -- Joey Hess <joeyh@debian.org>  Mon, 03 Nov 2008 18:50:03 -0500
+  [ Neil Williams ]
+  * Add experimental dh_gentdeb
+
+ -- Neil Williams <codehelp@debian.org>  Fri, 28 Nov 2008 18:17:03 +0100
 
 debhelper (7.1.0) experimental; urgency=low
 
diff --git a/dh_gentdeb b/dh_gentdeb
new file mode 100755 (executable)
index 0000000..756c339
--- /dev/null
@@ -0,0 +1,621 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+dh_gentdeb - build debian TDeb translation packages
+
+=cut
+
+use strict;
+use warnings;
+use Debian::Debhelper::Dh_Lib;
+# need to remove this module once debhelper recognises tdebs
+use Parse::DebControl;
+
+=head1 SYNOPSIS
+
+B<dh_gentdeb> [S<I<debhelper options>>]
+
+=head1 DESCRIPTION
+
+dh_gentdeb prepares localisation content for a debian Tdeb
+package.
+
+Only the -p debhelper option is handled by dh_gentdeb, if none is
+specified the TDeb package name will be '$sourcepackage-tdeb'.
+
+=cut
+
+=head1 DESCRIPTION
+
+dh_gentdeb is a debhelper add-on created by Emdebian to create
+translation packages (tdebs). dh_gentdeb is intended to separate
+out the individual translation files from the current Debian packages
+into packages without any translation files and a single TDeb package,
+one per source package.
+
+Generated packages use the syntax:
+ $srcpackage-tdeb_$version_all.tdeb
+
+If a second tdeb is supported by one source package, the $srcpackage-tdeb
+package must contain any debconf templates used by any of the binary
+packages. The second tdeb is then used for translations of optional
+content.
+
+(Note that Debian TDebs are architecture-independent, Emdebian TDebs
+are architecture-dependent.)
+
+Once a package uses dh_gentdeb, translation files must be removed
+from all packages in the normal build. This includes all translated
+manpages and other translated content. Original, untranslated, content
+should remain.
+
+dh_gentdeb runs as a part of the normal package build - simply add
+the call to the binary-indep target of debian/rules, usually after
+dh_install and before dh_builddeb. dh_gentdeb handles locating the
+relevant files, a .install file is not normally necessary.
+
+Support for a tdeb diff1.gz will be added as dh_gentdeb develops. 
+The extra diff is used by translators to build updated or new tdeb
+packages. Tdeb packages depend on the source:Version of the mainpackage
+but no packages may depend upon the tdeb. (Not even other TDebs). The
+mainpackage can be specified using the -p option.
+
+Use of diff1.gz should remove the need to create a customised source
+with a debian/rules stub etc. by allowing Emdebian TDebs to be created
+during an Emdebian build and Debian TDebs in a Debian build.
+Translators would then be able to use:
+
+ $ apt-get source $package
+ $ cd $package-$version/
+ $ poedit po/$lang.po
+
+To build the package, either dh_gentdeb can behave as em_installtdeb
+does now and run a build only of the TDeb components or
+dpkg-buildpackage could gain an option to only process the TDeb.
+
+dh_gentdeb currently only supports gettext translation.
+
+The locale package must use GETTEXT_PACKAGE for the eventual filename
+of the binary translation file - although this may be the same as the
+$dh{MAINPACKAGE}. GETTEXT_PACKAGE is determined by upstream, not Debian.
+When building the whole package, the binary translation file may be in
+debian/tmp/usr/share/locale/$lang/LC_MESSAGES but when in translator mode,
+this location is not available. Instead retrieve GETTEXT_PACKAGE from
+the POT filename, the Makefile GETTEXT_PACKAGE macro or if that is not set,
+use the upstream source package name. This may need extending.
+
+Some packages use multiple po directories and dh_gentdeb checks for
+a POT file in all usable po directories, including within the tdeb source,
+along with all po files: e.g.
+
+ po/fr.po
+ po-lib/fr.po
+ po/application.pot
+ po-lib/library.pot
+
+When packaged, the Debian tdeb built from this source would contain:
+
+ ./fr/usr/share/locale/fr/LC_MESSAGES/application.mo
+ ./de/usr/share/locale/de/LC_MESSAGES/application.mo
+ ./fr/usr/share/locale/fr/LC_MESSAGES/library.mo
+ ./de/usr/share/locale/de/LC_MESSAGES/library.mo
+
+For more detail on Tdebs, see:
+L<http://www.emdebian.org/emdebian/langupdate.html>
+L<http://wiki.debian.org/i18n/TranslationDebs>
+
+Note that the Debian implementation of tdebs differs from the 
+tdebs for Emdebian because Emdebian does not care about manpages in
+general, let alone translated manpages. Once the 'nodocs'
+DEB_BUILD_OPTION is supported in debhelper, this will not be an issue
+as the tdebs can be built for Emdebian without any manpages. Other
+translated documentation would be omitted under 'nodocs' too. Images
+containing translated text are relatively few.
+
+=cut
+
+=head1 OPTIONS
+
+The default action is to process all available po files and all
+identifiable translated content.
+
+=cut
+
+use vars qw/@packages $mainpackage $lang $fullname $tdebname %package_types
+$section $priority %lang_equiv $file %lang_codes @new_locales $topdirprefix
+$finprefix $finsuffix $single $gettext_package @podirs %gettextdirs $version
+$sign @names $builddir $source /;
+
+&init();
+&getpackages();
+
+# need data from debian/control even if xcontrol does not exist.
+my $parser = new Parse::DebControl;
+my $options = { stripComments => 1};
+my $xcontrol = $parser->parse_file('./debian/control', $options);
+
+for my $stanza (@$xcontrol)
+{
+       my $type = $stanza->{'XC-Package-Type'};
+       $source = $stanza->{'Source'} if (defined $stanza->{'Source'});
+       if (defined $type)
+       {
+               $package_types{$stanza->{'Package'}}=$type;
+       }
+}
+$tdebname = "${source}-tdeb";
+
+# Return true if a given package is really a tdeb.
+# Needs to be supported by Dh_Lib.pm alongside udeb,
+# then the Parse::DebianControl module can be dropped.
+sub is_tdeb {
+       my $package=shift;
+       return 0 unless (exists $package_types{$package});
+       return $package_types{$package} eq 'tdeb';
+}
+
+foreach my $package (@{$dh{DOPACKAGES}}) {
+       next unless is_tdeb ($package);
+       $tdebname = $package;
+}
+
+&parse_control;
+exit 0;
+
+sub get_gettext_names
+{
+       my $podir = shift;
+       # this fails if the package uses a build-tree.
+       if (-f "$podir/Makefile")
+       {
+               open (MK, "$podir/Makefile") or 
+                       die ("Failed to read $podir/Makefile: $!\n");
+               my @mkfile=<MK>;
+               close MK;
+               my @gp_ = grep /GETTEXT_PACKAGE*/, @mkfile;
+               foreach my $gp (@gp_)
+               {
+                       chomp($gp);
+                       $gp =~ s/ //g;
+                       if ($gp =~ /^GETTEXT_PACKAGE=(.*)$/)
+                       {
+                               return $1;
+                       }
+               }
+               @gp_ = grep /domainname*/i, @mkfile;
+               foreach my $gp (@gp_)
+               {
+                       chomp($gp);
+                       $gp =~ s/ //g;
+                       if ($gp =~ /^domainname=(.*)$/i)
+                       {
+                               return $1;
+                       }
+               }
+       }
+       else
+       {
+               opendir (POT, "$podir") or die ("Cannot open $podir\n");
+               my @potname=grep(/\.pot$/, readdir(POT));
+               closedir (POT);
+               if (@potname)
+               {
+                       my $name = $potname[0];
+                       $name =~ s/(.*)\.pot$/$1/;
+                       return $name if (defined ($name));
+               }
+       }
+       # if no po/Makefile exists, try the top_srcdir Makefile
+       if (-f "Makefile")
+       {
+               open (MK, "Makefile") or die ("Failed to read Makefile: $!\n");
+               my @mkfile=<MK>;
+               close MK;
+               my @gp_ = grep /GETTEXT_PACKAGE*/, @mkfile;
+               foreach my $gp (@gp_)
+               {
+                       chomp($gp);
+                       $gp =~ s/ //g;
+                       if ($gp =~ /^GETTEXT_PACKAGE=(.*)$/)
+                       {
+                               return $1;
+                       }
+               }
+       }
+       # if all this fails, use debian/xcontrol instead.
+       return $source;
+}
+
+sub check_debian
+{
+       my $pkg;
+       # check this is a debian working directory
+       until (-f "debian/changelog")
+       {
+               chdir ".." or die "Cannot change directory ../ $!";
+               if (cwd() eq '/')
+               {
+                       die "Cannot find debian/changelog anywhere!\nAre you in the source code tree?\n";
+               }
+       }
+       my $clog = `dpkg-parsechangelog`;
+       my $r = $clog;
+       $clog =~ /Version: (.*)\n/;
+       $version = $1;
+       # strip epoch
+       $version =~ s/[0-9]://;
+       # try to assume that po/ exists
+       @podirs= `find . -name 'po*' -a -type d`;
+       my @templist = ();
+       foreach my $podir (@podirs)
+       {
+               chomp($podir);
+               next unless $podir =~ /\/po[-]?.*/;
+               next unless (-d $podir);
+               # TODO: handle manpage translations source  - wrap in nodocs support.
+               if (($podir =~ m#manpage#) or ($podir =~ m#^\./debian/po#)
+                       or ($podir =~ m#man#))
+               {
+                       next;
+#                      print "DEBUG: found PO content in $podir - needs to go into src package or diff1.gz\n";
+               }
+               push @templist, $podir;
+       }
+       @podirs = @templist;
+       # if custom support requested, avoid tampering with POT files.
+       return if (@names);
+       foreach my $podir (@podirs)
+       {
+               my @potfiles= `find $podir -maxdepth 1 -name '*po' -type f`;
+               next unless @potfiles;
+               $gettext_package = &get_gettext_names($podir);
+               # one package per po directory
+               $gettextdirs{$gettext_package} = $podir;
+               if (scalar @potfiles == 0)
+               {
+                       # if no POT file exists, try to make it.
+                       # the Makefile in the $podir always uses GETTEXT_PACKAGE
+                       # for *this* POT file even if the top_srcdir Makefile
+                       # uses more than one GETTEXT variable.
+                       system ("make -C $podir $gettext_package.pot") if (-f "$podir/Makefile");
+               }
+               die "Cannot find POT file in $podir!\n"
+                       if (not defined $gettext_package);
+       }
+}
+
+sub parse_xcontrol
+{
+       my $xcontrol;
+       return $xcontrol if (! -f "debian/xcontrol");
+       my $parser = new Parse::DebControl;
+       my $options;
+       $xcontrol = $parser->parse_file('./debian/xcontrol', $options);
+       return $xcontrol;
+}
+
+sub find_messages
+{
+       my $code;
+       my $v = "";
+       # skip our own packages
+       return if ($_[0] =~ m:/debian/:);
+       # gettext dirs *should* be the same as podirs but not all
+       # packages play by those rules.
+       foreach my $podir (@podirs)
+       {
+               chomp ($podir);
+               next unless $podir =~ /\/po[-]?.*/;
+               next unless (-d $podir);
+               my @pofiles=`find $podir -name '*po' -a -type f`;
+               next unless @pofiles;
+               foreach my $pofile (@pofiles)
+               {
+                       chomp($pofile);
+                       my $c = basename ($pofile);
+                       $pofile = $c;
+                       $pofile =~ /^(.*)\.po$/;
+                       my $a = $code = $1;
+                       next unless (defined ($code));
+                       $code =~ s/[_]/-/;
+                       $code =~ s/[@]/+/;
+                       $code = lc ($code);
+                       $code =~ s/\/.*//;
+                       $a =~ s/\/.*//;
+                       # if a package has more than one translation, only set one lang_code
+                       $lang_codes{$code} = 1;
+                       $lang_equiv{$code} = $a;
+               }
+       }
+}
+
+sub parse_control
+{
+       my @package_list = ();
+       my $pkg;
+       @names=();
+       my $xcontrol = &parse_xcontrol;
+       # only interested in top stanza (Source: )
+       my $stanza = $$xcontrol[0];
+       if (defined $stanza->{'XS-TDeb-Build-Directory'} or
+               defined $stanza->{'XS-TDeb-POT-Names'})
+       {
+               $builddir = $stanza->{'XS-TDeb-Build-Directory'};
+               my @tmp = split(',', $stanza->{'XS-TDeb-POT-Names'});
+               foreach my $n (@tmp)
+               {
+                       $n =~ s/^\s+//;
+                       push @names, $n;
+               }
+       }
+       # check if a changelog exists
+       &check_debian;
+       &find_messages($source);
+       # check to prevent duplication
+       my @sorted = sort (keys %lang_codes);
+       # nothing to do if @sorted is empty, unless templates exist
+       return if ((not @sorted) and (not -d "debian/po"));
+       foreach $lang (@sorted)
+       {
+               $fullname = $mainpackage;
+               &install_mofiles($lang);
+       }
+       &add_content();
+#      &build_tdeb();
+}
+
+# look for and add other translated content
+# needs Dpkg::Class support
+sub add_content
+{
+       my @cmds = ();
+       $topdirprefix="debian/${tdebname}";
+       push @cmds, "install -d ${topdirprefix}/DEBIAN"
+               if (not -d "${topdirprefix}/DEBIAN");
+       my $contentprefix = "usr/share/"; # tdeb content should always be usr/share ?
+       my $location = "debian/tmp/${contentprefix}";
+       $location = "debian/${source}/${contentprefix}" if (not -d $location);
+       my $destination = "${topdirprefix}/${contentprefix}";
+       # debconf template handling - needs testing and dpkg support.
+
+=head1 Debconf Templates
+
+Packages may need to rename the templates file for the template file
+and change the reference in debian/po/POTFILES.in to the new file. This
+results in a lintian warning:
+
+ Now running lintian...
+ W: dpkg-cross: no-debconf-templates
+ Finished running lintian.
+
+The package probably now needs to Pre-Depend on the TDeb.
+Alternatively either dpkg or debconf should automatically install a
+TDeb prior to trying to configure the main package.
+
+Templates files are the most common reason for l10n rebuilds of
+packages prior to a release.
+
+=cut
+
+       my @templ_loc = `find debian -maxdepth 1 -name *templates`;
+       chomp (@templ_loc);
+       foreach my $line (@templ_loc)
+       {
+               next if grep (/udeb/, $line);
+               print "Migrating $line into TDeb.\n";
+               push @cmds, "install -m 0644 $line ${topdirprefix}/DEBIAN/templates"
+                       if ((-f "$line") and ("$line" ne "${topdirprefix}/DEBIAN/templates"));
+       }
+       my @contentlist = qw: man info :;
+       my %removals=();
+       $destination = "${topdirprefix}";
+       foreach my $dir (@contentlist)
+       {
+               next if (! -d "$location$dir");
+               opendir (CONTENT, "$location$dir")
+                       or die ("Unable to open existing directory $location$dir: $!\n");
+               my @files=grep(!/^\.\.?$/, readdir (CONTENT));
+               closedir (CONTENT);
+               foreach my $cdir (@files)
+               {
+                       my $clocation = "$location${dir}/$cdir";
+                       my $cdestination = "${cdir}/${dir}";
+                       # skip untranslated content
+                       next if ($cdir =~ /^man[0-9]$/);
+                       opendir (TRANS, "$clocation/");
+                       my @tdirs=grep(!/^\.\.?$/, readdir (TRANS));
+                       closedir (TRANS);
+                       foreach my $tdir (@tdirs)
+                       {
+                               my $troot = $cdir;
+                               $troot =~ s/[-_].*$//;
+                               my $tlocation = "$clocation/$tdir";
+                               $removals{$clocation}++;
+                               my $tdestination = "$destination/$troot/${contentprefix}$cdestination/$tdir";
+                               next if (-f "$tlocation");
+                               push @cmds, "install -d $tdestination";
+                               opendir (FILES, "$tlocation");
+                               my @tfiles=grep(!/^\.\.?$/, readdir (FILES));
+                               closedir (FILES);
+                               foreach my $tfile (@tfiles)
+                               {
+                                       push @cmds, "install -m 0644 $tlocation/$tfile $tdestination"
+                               }
+                       }
+               }
+       }
+       foreach my $cmd (@cmds)
+       {
+               system ("$cmd");
+       }
+       # HACK ALERT! this system rm -rf must not survive into the released
+       # script.
+       # Either needs to be a recursive unlink or, even better, a fix
+       # in scripts like dh_installman so that the installing script
+       # understands the needs of the tdeb.
+       foreach my $rem (keys %removals)
+       {
+               system ("rm -rf $rem");
+       }
+       undef @cmds;
+}
+
+sub install_mofiles
+{
+       my $lang=shift;
+       $topdirprefix="debian/${tdebname}";
+       $finprefix="/usr/share/locale/";
+       $finsuffix="/LC_MESSAGES";
+       my $tmppo = $lang_equiv{$lang};
+       my $tdebprefix = $lang;
+       $tdebprefix =~ s/_.*$//;
+       $tdebprefix =~ s/-.*$//;
+       my @cmds=();
+       push @cmds, "install -d ${topdirprefix}/DEBIAN";
+       foreach my $cmd (@cmds)
+       {
+               system ($cmd);
+       }
+       @cmds=();
+       open (ORIG, "debian/control") or
+               die ("Cannot open debian/control: $!\n");
+       my @lines=<ORIG>;
+       close (ORIG);
+       open (DEB, ">${topdirprefix}/DEBIAN/control") or
+               die ("Cannot open ${topdirprefix}/DEBIAN/control: $!\n");
+       print DEB @lines;
+       close DEB;
+       push @cmds, "install -d ${topdirprefix}/${tdebprefix}/${finprefix}${tmppo}${finsuffix}"
+               if (defined keys %gettextdirs);
+       foreach my $gpkg (keys %gettextdirs)
+       {
+               my $pdir = $gettextdirs{$gpkg};
+               next unless $pdir =~ /\/po[-]?.*/;
+               next unless (-d $pdir);
+               next unless (-f "$pdir/$tmppo.po");
+               push @cmds, "msgfmt -o $pdir/$tmppo.gmo $pdir/$tmppo.po" if (! -f "$pdir/$tmppo.gmo");
+               push @cmds, "install -m 0644 $pdir/$tmppo.gmo ${topdirprefix}/${tdebprefix}/${finprefix}${tmppo}".
+                       "${finsuffix}/${gpkg}.mo";
+       }
+       # some packages, like apt, use specialised handling which is
+       # supported using fields in debian/xcontrol.
+       if ((defined (@names)) and (defined ($builddir)))
+       {
+               push @cmds, "install -d ${topdirprefix}/${tdebprefix}/${finprefix}${tmppo}${finsuffix}"
+                       if (defined @names);
+               foreach my $d (@names)
+               {
+                       next unless (-d "$builddir/$d/");
+                       my @custom_po = `find $builddir/$d/ -name $tmppo\.mo -type f`;
+                       foreach my $custom (@custom_po)
+                       {
+                               chomp ($custom);
+                               push @cmds, "install -m 0644 $custom ${topdirprefix}/${tdebprefix}/${finprefix}${tmppo}".
+                                       "${finsuffix}/$d.mo";
+                       }
+               }
+       }
+       # do the real work here.
+       foreach my $cmd (@cmds)
+       {
+               system ("$cmd");
+       }
+       undef @cmds;
+}
+
+# code replaced by dpkg tdeb handlers
+sub build_tdeb
+{
+       my @cmds=();
+       push @cmds, "dpkg-gencontrol ".
+               "-p${mainpackage} ".
+               "-P${topdirprefix} -cdebian/control";
+
+       # XXX - debhelper bug. dh_builddeb fails to accept
+       # XC-Package-Type: tdeb, only udeb.
+       # Once debhelper fixed, remove the call to dpkg --build.
+       
+       my $name = "../${mainpackage}_${version}_all.tdeb";
+#      print "DEBUG: dpkg --build ${topdirprefix} $name ";
+       push @cmds, "dpkg --build ${topdirprefix} $name ";
+       # add clean up commands.
+#      push @cmds, "rm -rf debian/${mainpackage}-locale*";
+       push @cmds, "rm -f po*/*.gmo";
+       # need to handle $dh{NO_ACT}
+       # do the real work here.
+       foreach my $cmd (@cmds)
+       {
+               system ("$cmd");
+       }
+       undef @cmds;
+}
+
+=head1 SEE ALSO
+
+debhelper (7)
+
+This program is based on debhelper.
+
+=head1 AUTHOR
+
+Neil Williams <codehelp@debian.org>
+
+=cut
+
+=head1 Copyright and Licence
+
+ Copyright (C) 2007-2008  Neil Williams <codehelp@debian.org>
+
+ This package 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.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+=cut
+
+=head1 Use in Debian
+
+generate_source will be removed before inclusion into Debian.
+
+At the same time, XC-Package-Type: tdeb needs support too. Notably,
+many of the scripts in the devscripts package fail to identify the
+TDeb in the .changes file and certain debhelper scripts fail to
+handle the TDeb package-type.
+
+reprepro needs a patch to accept .tdeb and allow .tdeb in
+the repository files:
+ $ reprepro --ignore=extension -b /path/ includedeb \
+ unstable ../qof-locale-sv_0.7.5-1em1_arm.tdeb
+ $ ls /opt/reprepro/locale/pool/main/q/qof/
+ qof-locale-sv_0.7.5-1em1_arm.deb
+
+ Filename: pool/main/q/qof/qof-locale-sv_0.7.5-1em1_arm.deb
+ Description: sv translation for qof (tdeb)
+
+reprepro also needs a way to handle a .tdeb in a .changes file.
+ reprepro -b /opt/reprepro/locale/ include unstable ../qof_0.7.5-1em1_arm.changes
+ 'qof-locale-id_0.7.5-1em1_arm.tdeb' is not .deb or .udeb!
+ There have been errors!
+
+=cut
+
+=head1 Other translations
+
+Packages may also contain translated manpages and translations for
+debconf templates. These translations are not yet packaged or processed
+by dpkg-gentdeb. For Tdebs to be supported in Debian, these issues will
+need to be resolved such that Emdebian can continue to only package the
+gettext program translations, omitting translated manpages and leaving
+debconf translation support to existing tools or implement sufficient
+changes in cdebconf.
+
+=cut