# debian/po-debconf -- lintian check script -*- perl -*- # Copyright (C) 2002-2004 by Denis Barbier # Copyright (C) 2021 Felix Lechner # # 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 2 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, you can find it on the World Wide # Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, # MA 02110-1301, USA. package Lintian::Check::Debian::PoDebconf; use v5.20; use warnings; use utf8; use Const::Fast; use Cwd qw(realpath); use File::Temp(); use IPC::Run3; use Syntax::Keyword::Try; use Unicode::UTF8 qw(encode_utf8); use Lintian::IPC::Run3 qw(safe_qx); use Moo; use namespace::clean; with 'Lintian::Check'; const my $EMPTY => q{}; sub source { my ($self) = @_; my $processable = $self->processable; my $has_template = 0; my @lang_templates; my $full_translation = 0; my $debian_dir = $processable->patched->resolve_path('debian/'); return unless $debian_dir; my $debian_po_dir = $debian_dir->resolve_path('po'); my ($templ_pot_path, $potfiles_in_path); if ($debian_po_dir and $debian_po_dir->is_dir) { $templ_pot_path = $debian_po_dir->resolve_path('templates.pot'); $potfiles_in_path = $debian_po_dir->resolve_path('POTFILES.in'); } # First, check whether this package seems to use debconf but not # po-debconf. Read the templates file and look at the template # names it provides, since some shared templates aren't # translated. for my $item ($debian_dir->children) { next unless $item->is_open_ok; if ($item->basename =~ m/^(.+\.)?templates(\..+)?$/) { if ($item->basename =~ m/templates\.\w\w(_\w\w)?$/) { push(@lang_templates, $item); open(my $fd, '<', $item->unpacked_path) or die encode_utf8('Cannot open ' . $item->unpacked_path); while (my $line = <$fd>) { $self->pointed_hint('untranslatable-debconf-templates', $item->pointer($.)) if $line =~ /^Description: (.+)/i && $1 !~/for internal use/; } close $fd; } else { open(my $fd, '<', $item->unpacked_path) or die encode_utf8('Cannot open ' . $item->unpacked_path); my $in_template = 0; my $saw_tl_note = 0; while (my $line = <$fd>) { chomp $line; $self->pointed_hint('translated-default-field', $item->pointer($.)) if $line =~ m{^_Default(?:Choice)?: [^\[]*$} && !$saw_tl_note; $self->pointed_hint('untranslatable-debconf-templates', $item->pointer($.)) if $line =~ /^Description: (.+)/i && $1 !~/for internal use/; if ($line =~ /^#/) { # Is this a comment for the translators? $saw_tl_note = 1 if $line =~ /translators/i; next; } # If it is not a continuous comment immediately before the # _Default(Choice) field, we don't care about it. $saw_tl_note = 0; if ($line =~ /^Template: (\S+)/i) { my $template = $1; next if $template eq 'shared/packages-wordlist' or $template eq 'shared/packages-ispell'; next if $template =~ m{/languages$}; $in_template = 1; } elsif ($in_template && $line =~ /^_?Description: (.+)/i){ my $description = $1; next if $description =~ /for internal use/; $has_template = 1; } elsif ($in_template && !length($line)) { $in_template = 0; } } close($fd); } } } #TODO: check whether all templates are named in TEMPLATES.pot if ($has_template) { if (not $debian_po_dir or not $debian_po_dir->is_dir) { $self->hint('not-using-po-debconf'); return; } } else { return; } # If we got here, we're using po-debconf, so there shouldn't be any stray # language templates left over from debconf-mergetemplate. for my $item (@lang_templates) { $self->pointed_hint('stray-translated-debconf-templates', $item->pointer) unless $item->basename =~ m{ templates[.]in$}x; } my $missing_files = 0; if ($potfiles_in_path and $potfiles_in_path->is_open_ok) { open(my $fd, '<', $potfiles_in_path->unpacked_path) or die encode_utf8('Cannot open ' . $potfiles_in_path->unpacked_path); my $position = 1; while (my $line = <$fd>) { chomp $line; next if $line =~ /^\s*\#/; $line =~ s/.*\]\s*//; # Cannot check files which are not under debian/ # m,^\.\./, or next if $line eq $EMPTY; my $pointer = $potfiles_in_path->pointer($position); my $po_path = $debian_dir->resolve_path($line); unless ($po_path and $po_path->is_file) { $self->pointed_hint('missing-file-from-potfiles-in', $pointer, $line); $missing_files = 1; } } continue { ++$position; } close $fd; } else { $self->hint('missing-potfiles-in'); $missing_files = 1; } if (not $templ_pot_path or not $templ_pot_path->is_open_ok) { # We use is_open_ok here, because if it is present, we will # (have a subprocess) open it if the POTFILES.in file also # existed. $self->hint('missing-templates-pot'); $missing_files = 1; } if ($missing_files == 0) { my $temp_obj = File::Temp->newdir('lintian-po-debconf-XXXXXX',TMPDIR => 1); my $abs_tempdir = realpath($temp_obj->dirname) or croak('Cannot resolve ' . $temp_obj->dirname . ": $!"); # We need an extra level of dirs, as intltool (in)directly # tries to use files in ".." if they exist # (e.g. ../templates.h). # - In fact, we also need to copy debian/templates into # this "fake package directory", since intltool-updates # sometimes want to write files to "../templates" based # on the contents of the package. (See #778558) my $tempdir = "$abs_tempdir/po"; my $test_pot = "$tempdir/test.pot"; my $tempdir_templates = "${abs_tempdir}/templates"; my $d_templates = $debian_dir->resolve_path('templates'); # Create our extra level mkdir($tempdir) or die encode_utf8('Cannot create directory ' . $tempdir); # Copy the templates dir because intltool-update might # write to it. safe_qx( qw{cp -a --reflink=auto --}, $d_templates->unpacked_path, $tempdir_templates )if $d_templates; my $error; my %save = %ENV; my $cwd = Cwd::getcwd; try { $ENV{INTLTOOL_EXTRACT} = '/usr/share/intltool-debian/intltool-extract'; # use of $debian_po is safe; we accessed two children by now. $ENV{srcdir} = $debian_po_dir->unpacked_path; chdir($tempdir) or die encode_utf8('Cannot change directory ' . $tempdir); # generate a "test.pot" in a tempdir my @intltool = ( '/usr/share/intltool-debian/intltool-update', '--gettext-package=test','--pot' ); safe_qx(@intltool); die encode_utf8("system @intltool failed: $?") if $?; } catch { # catch any error $error = $@; } finally { # restore environment %ENV = %save; # restore working directory chdir($cwd) or die encode_utf8('Cannot change directory ' . $cwd); } # output could be helpful to user but is currently not printed if ($error) { $self->pointed_hint('invalid-potfiles-in', $potfiles_in_path->pointer); return; } # throw away output on the following commands $error = undef; try { # compare our "test.pot" with the existing "templates.pot" my @testleft = ( 'msgcmp', '--use-untranslated', $test_pot, $templ_pot_path->unpacked_path ); safe_qx(@testleft); die encode_utf8("system @testleft failed: $?") if $?; # is this not equivalent to the previous command? - FL my @testright = ( 'msgcmp', '--use-untranslated', $templ_pot_path->unpacked_path, $test_pot ); safe_qx(@testright); die encode_utf8("system @testright failed: $?") if $?; } catch { # catch any error $error = $@; } $self->pointed_hint('newer-debconf-templates',$templ_pot_path->pointer) if length $error; } return unless $debian_po_dir; for my $po_item ($debian_po_dir->children) { next unless $po_item->basename =~ m/\.po$/ || $po_item->is_dir; $self->pointed_hint('misnamed-po-file', $po_item->pointer) unless ( $po_item->basename =~ /^[a-z]{2,3}(_[A-Z]{2})?(?:\@[^\.]+)?\.po$/); next unless $po_item->is_open_ok; my $bytes = $po_item->bytes; $self->pointed_hint('debconf-translation-using-general-list', $po_item->pointer) if $bytes =~ /Language\-Team:.*debian-i18n\@lists\.debian\.org/i; unless ($bytes =~ /^msgstr/m) { $self->pointed_hint('invalid-po-file', $po_item->pointer); next; } if ($bytes =~ /charset=(.*?)\\n/) { my $charset = ($1 eq 'CHARSET' ? $EMPTY : $1); $self->pointed_hint('unknown-encoding-in-po-file', $po_item->pointer) unless length $charset; } my $error; my $stats; delete local $ENV{$_} for grep { $_ ne 'PATH' && $_ ne 'TMPDIR' } keys %ENV; local $ENV{LC_ALL} = 'C'; my @command = ( 'msgfmt', '-o', '/dev/null', '--statistics', $po_item->unpacked_path ); run3(\@command, \undef, \undef, \$stats); $self->pointed_hint('invalid-po-file', $po_item->pointer) if $?; $stats //= $EMPTY; $full_translation = 1 if $stats =~ m/^\w+ \w+ \w+\.$/; } $self->hint('no-complete-debconf-translation') if !$full_translation; return; } 1; # Local Variables: # indent-tabs-mode: nil # cperl-indent-level: 4 # End: # vim: syntax=perl sw=4 sts=4 sr et