diff options
Diffstat (limited to '')
-rw-r--r-- | lib/Lintian/Check/Debian/Rules.pm | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/lib/Lintian/Check/Debian/Rules.pm b/lib/Lintian/Check/Debian/Rules.pm new file mode 100644 index 0000000..ffae6cb --- /dev/null +++ b/lib/Lintian/Check/Debian/Rules.pm @@ -0,0 +1,671 @@ +# debian/rules -- lintian check script -*- perl -*- + +# Copyright (C) 2006 Russ Allbery <rra@debian.org> +# Copyright (C) 2005 Rene van Bevern <rvb@pro-linux.de> +# Copyright (C) 2019-2020 Chris Lamb <lamby@debian.org> +# 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. + +package Lintian::Check::Debian::Rules; + +use v5.20; +use warnings; +use utf8; + +use Carp qw(croak); +use Const::Fast; +use List::Compare; +use List::SomeUtils qw(any none uniq); +use Unicode::UTF8 qw(encode_utf8); + +use Moo; +use namespace::clean; + +with 'Lintian::Check'; + +const my $EMPTY => q{}; +const my $SPACE => q{ }; +const my $PERCENT => q{%}; + +my @py3versions = qw(3.4 3.5 3.6 3.7); + +my $PYTHON_DEPEND= 'python2:any | python2-dev:any'; +my $PYTHON3_DEPEND + = 'python3:any | python3-dev:any | python3-all:any | python3-all-dev:any'; +my $PYTHON2X_DEPEND = 'python2.7:any | python2.7-dev:any'; +my $PYTHON3X_DEPEND + = join(' | ',map { "python${_}:any | python${_}-dev:any" } @py3versions); +my $ANYPYTHON_DEPEND + = "$PYTHON_DEPEND | $PYTHON2X_DEPEND | $PYTHON3_DEPEND | $PYTHON3X_DEPEND"; +my $PYTHON3_ALL_DEPEND + = 'python3-all:any | python3-all-dev:any | python3-all-dbg:any'; + +my %TAG_FOR_POLICY_TARGET = ( + build => 'debian-rules-missing-required-target', + binary => 'debian-rules-missing-required-target', + 'binary-arch' => 'debian-rules-missing-required-target', + 'binary-indep' => 'debian-rules-missing-required-target', + clean => 'debian-rules-missing-required-target', + 'build-arch' => 'debian-rules-missing-required-target', + 'build-indep' => 'debian-rules-missing-required-target' +); + +# Rules about required debhelper command ordering. Each command is put into a +# class and the tag is issued if they're called in the wrong order for the +# classes. Unknown commands won't trigger this flag. +my %debhelper_order = ( + dh_makeshlibs => 1, + dh_shlibdeps => 2, + dh_installdeb => 2, + dh_gencontrol => 2, + dh_builddeb => 3 +); + +sub source { + my ($self) = @_; + + my $debian_dir = $self->processable->patched->resolve_path('debian'); + + my $rules; + $rules = $debian_dir->child('rules') + if defined $debian_dir; + + return + unless defined $rules; + + # Policy could be read as allowing debian/rules to be a symlink to + # some other file, and in a native Debian package it could be a + # symlink to a file that we didn't unpack. + $self->pointed_hint('debian-rules-is-symlink', $rules->pointer) + if $rules->is_symlink; + + # dereference symbolic links + $rules = $rules->follow; + + return + unless defined $rules; + + $self->pointed_hint('debian-rules-not-executable', $rules->pointer) + unless $rules->is_executable; + + my $KNOWN_MAKEFILES= $self->data->load('rules/known-makefiles', '\|\|'); + my $DEPRECATED_MAKEFILES= $self->data->load('rules/deprecated-makefiles'); + + my $architecture = $self->processable->fields->value('Architecture'); + + # If the version field is missing, we assume a neutral non-native one. + my $version = $self->processable->fields->value('Version') || '0-1'; + + # Check for required #!/usr/bin/make -f opening line. Allow -r or -e; a + # strict reading of Policy doesn't allow either, but they seem harmless. + $self->pointed_hint('debian-rules-not-a-makefile', $rules->pointer) + unless $rules->hashbang =~ m{^/usr/bin/make\s+-[re]?f[re]?$}; + + # Certain build tools must be listed in Build-Depends even if there are no + # arch-specific packages because they're required in order to run the clean + # rule. (See Policy 7.6.) The following is a list of package dependencies; + # regular expressions that, if they match anywhere in the debian/rules file, + # say that this package is allowed (and required) in Build-Depends; and + # optional tags to use for reporting the problem if some information other + # than the default is required. + my %GLOBAL_CLEAN_DEPENDS = ( + 'ant:any' => [qr{^include\s*/usr/share/cdbs/1/rules/ant\.mk}], + 'cdbs:any' => [ + qr{^include\s+/usr/share/cdbs/}, + qr{^include\s+/usr/share/R/debian/r-cran\.mk} + ], + 'dbs:any' => [qr{^include\s+/usr/share/dbs/}], + 'dh-make-php:any' => [qr{^include\s+/usr/share/cdbs/1/class/pear\.mk}], + 'debhelper:any | debhelper-compat:any' =>[ + qr{^include\s+/usr/share/cdbs/1/rules/debhelper\.mk}, + qr{^include\s+/usr/share/R/debian/r-cran\.mk} + ], + 'dpatch:any' => [ + qr{^include\s+/usr/share/dpatch/}, + qr{^include\s+/usr/share/cdbs/1/rules/dpatch\.mk} + ], + 'gnome-pkg-tools:any | dh-sequence-gnome:any' => + [qr{^include\s+/usr/share/gnome-pkg-tools/}], + 'quilt:any' => [ + qr{^include\s+/usr/share/quilt/}, + qr{^include\s+/usr/share/cdbs/1/rules/patchsys-quilt\.mk} + ], + 'mozilla-devscripts:any' => + [qr{^include\s+/usr/share/mozilla-devscripts/}], + 'ruby-pkg-tools:any' => + [qr{^include\s+/usr/share/ruby-pkg-tools/1/class/}], + 'r-base-dev:any' => [qr{^include\s+/usr/share/R/debian/r-cran\.mk}], + $ANYPYTHON_DEPEND =>[qr{/usr/share/cdbs/1/class/python-distutils\.mk}], + ); + + # A list of packages; regular expressions that, if they match anywhere in the + # debian/rules file, this package must be listed in either Build-Depends or + # Build-Depends-Indep as appropriate; and optional tags as above. + my %GLOBAL_DEPENDS = ( + 'dh-ocaml:any, ocaml-nox:any | ocaml:any' => + [qr/^\t\s*dh_ocaml(?:init|doc)\s/], + 'debhelper:any | debhelper-compat:any | dh-autoreconf:any' => + [qr/^\t\s*dh_autoreconf(?:_clean)?\s/], + ); + + # Similarly, this list of packages, regexes, and optional tags say that if the + # regex matches in one of clean, build-arch, binary-arch, or a rule they + # depend on, this package is allowed (and required) in Build-Depends. + my %RULE_CLEAN_DEPENDS =( + 'ant:any' => [qr/^\t\s*(\S+=\S+\s+)*ant\s/], + 'debhelper:any | debhelper-compat:any' => + [qr/^\t\s*dh_(?!autoreconf).+/], + 'dh-ocaml:any, ocaml-nox:any | ocaml:any' =>[qr/^\t\s*dh_ocamlinit\s/], + 'dpatch:any' => [qr/^\t\s*(\S+=\S+\s+)*dpatch\s/], + 'po-debconf:any' => [qr/^\t\s*debconf-updatepo\s/], + $PYTHON_DEPEND => [qr/^\t\s*python\s/], + $PYTHON3_DEPEND => [qr/^\t\s*python3\s/], + $ANYPYTHON_DEPEND => [qr/\ssetup\.py\b/], + 'quilt:any' => [qr/^\t\s*(\S+=\S+\s+)*quilt\s/], + ); + + my $build_all = $self->processable->relation('Build-Depends-All'); + my $build_all_norestriction + = $self->processable->relation_norestriction('Build-Depends-All'); + my $build_regular = $self->processable->relation('Build-Depends'); + my $build_indep = $self->processable->relation('Build-Depends-Indep'); + + # no need to look for items we have + delete %GLOBAL_DEPENDS{$_} + for grep { $build_regular->satisfies($_) } keys %GLOBAL_DEPENDS; + delete %GLOBAL_CLEAN_DEPENDS{$_} + for grep { $build_regular->satisfies($_) } keys %GLOBAL_CLEAN_DEPENDS; + delete %RULE_CLEAN_DEPENDS{$_} + for grep { $build_regular->satisfies($_) } keys %RULE_CLEAN_DEPENDS; + + my @needed; + my @needed_clean; + + # Scan debian/rules. We would really like to let make do this for + # us, but unfortunately there doesn't seem to be a way to get make + # to syntax-check and analyze a makefile without running at least + # $(shell) commands. + # + # We skip some of the rule analysis if debian/rules includes any + # other files, since to chase all includes we'd have to have all + # of its build dependencies installed. + local $_ = undef; + + my @arch_rules = map { qr/^$_$/ } qw(clean binary-arch build-arch); + my @indep_rules = qw(build build-indep binary-indep); + my @current_targets; + my %rules_per_target; + my %debhelper_group; + my %seen; + my %overridden; + my $maybe_skipping; + my @conditionals; + my %variables; + my $includes = 0; + + my $contents = $rules->decoded_utf8; + return + unless length $contents; + + my @lines = split(/\n/, $contents); + + my $continued = $EMPTY; + my $position = 1; + + for my $line (@lines) { + + my $pointer = $rules->pointer($position); + + $self->pointed_hint('debian-rules-is-dh_make-template', $pointer) + if $line =~ m/dh_make generated override targets/; + + next + if $line =~ /^\s*\#/; + + if (length $continued) { + $line = $continued . $line; + $continued = $EMPTY; + } + + if ($line =~ s/\\$//) { + $continued = $line; + next; + } + + if ($line =~ /^\s*[s-]?include\s+(\S++)/){ + my $makefile = $1; + my $targets = $KNOWN_MAKEFILES->value($makefile); + if (defined $targets){ + for my $target (split /\s*+,\s*+/, $targets){ + $seen{$target}++ if exists $TAG_FOR_POLICY_TARGET{$target}; + } + } else { + $includes = 1; + } + + $self->pointed_hint('debian-rules-uses-deprecated-makefile', + $pointer, $makefile) + if $DEPRECATED_MAKEFILES->recognizes($makefile); + } + + # problems occurring only outside targets + unless (%seen) { + + # Check for DH_COMPAT settings outside of any rule, which are now + # deprecated. It's a bit easier structurally to do this here than in + # debhelper. + $self->pointed_hint('debian-rules-sets-DH_COMPAT', $pointer) + if $line =~ /^\s*(?:export\s+)?DH_COMPAT\s*:?=/; + + $self->pointed_hint('debian-rules-sets-DEB_BUILD_OPTIONS',$pointer) + if $line =~ /^\s*(?:export\s+)?DEB_BUILD_OPTIONS\s*:?=/; + + if ( + $line =~m{^ + \s*(?:export\s+)? + (DEB_(?:HOST|BUILD|TARGET)_(?:ARCH|MULTIARCH|GNU)[A-Z_]*)\s*:?= + }x + ) { + my $variable = $1; + + $self->pointed_hint( + 'debian-rules-sets-dpkg-architecture-variable', + $pointer, $variable); + } + + } + + if ( $line =~ /^\t\s*-(?:\$[\(\{]MAKE[\}\)]|make)\s.*(?:dist)?clean/s + || $line + =~ /^\t\s*(?:\$[\(\{]MAKE[\}\)]|make)\s(?:.*\s)?-(\w*)i.*(?:dist)?clean/s + ) { + my $flags = $1 // $EMPTY; + + # Ignore "-C<dir>" (#671537) + $self->pointed_hint('debian-rules-ignores-make-clean-error', + $pointer) + unless $flags =~ /^C/; + } + + if ($line + =~ m{dh_strip\b.*(--(?:ddeb|dbgsym)-migration=(?:'[^']*'|\S*))}) { + + my $context = $1; + + $self->pointed_hint('debug-symbol-migration-possibly-complete', + $pointer, $context); + } + + $self->pointed_hint('debian-rules-passes-version-info-to-dh_shlibdeps', + $pointer) + if $line =~ m{dh_shlibdeps\b.*(?:--version-info|-V)\b}; + + $self->pointed_hint('debian-rules-updates-control-automatically', + $pointer) + if $line =~ m{^\s*DEB_AUTO_UPDATE_DEBIAN_CONTROL\s*=\s*yes}; + + $self->pointed_hint('debian-rules-uses-deb-build-opts', $pointer) + if $line =~ m{\$[\(\{]DEB_BUILD_OPTS[\)\}]}; + + if ($line =~ m{^\s*DH_EXTRA_ADDONS\s*=\s*(.*)$}) { + + my $context = $1; + + $self->pointed_hint('debian-rules-should-not-use-DH_EXTRA_ADDONS', + $pointer, $context); + } + + $self->pointed_hint('debian-rules-uses-wrong-environment-variable', + $pointer) + if $line =~ m{\bDEB_[^_ \t]+FLAGS_(?:SET|APPEND)\b}; + + $self->pointed_hint('debian-rules-calls-pwd', $pointer) + if $line =~ m{\$[\(\{]PWD[\)\}]}; + + $self->pointed_hint( + 'debian-rules-should-not-use-sanitize-all-buildflag',$pointer) + if $line + =~ m{^\s*(?:export\s+)?DEB_BUILD_MAINT_OPTIONS\s*:?=.*\bsanitize=\+all\b}; + + $self->pointed_hint('debian-rules-uses-special-shell-variable', + $pointer) + if $line =~ m{\$[\(\{]_[\)\}]}; + + if ($line =~ m{(dh_builddeb\b.*--.*-[zZS].*)$}) { + + my $context = $1; + + $self->pointed_hint('custom-compression-in-debian-rules', + $pointer, $context); + } + + if ($line =~ m{(py3versions\s+([\w\-\s]*--installed|-\w*i\w*))}) { + + my $context = $1; + + $self->pointed_hint('debian-rules-uses-installed-python-versions', + $pointer, $context); + } + + $self->pointed_hint('debian-rules-uses-as-needed-linker-flag',$pointer) + if $line =~ /--as-needed/ && $line !~ /--no-as-needed/; + + if ($line =~ /(py3versions\s+([\w\-\s]*--supported|-\w*s\w*))/) { + + my $context = $1; + + $self->pointed_hint( +'debian-rules-uses-supported-python-versions-without-python-all-build-depends', + $pointer, + $context + )unless $build_all_norestriction->satisfies($PYTHON3_ALL_DEPEND); + } + + # General assignment - save the variable + if ($line =~ /^\s*(?:\S+\s+)*?(\S+)\s*[:\?\+]?=\s*(.*+)?$/s) { + # This is far too simple from a theoretical PoV, but should do + # rather well. + my ($var, $value) = ($1, $2); + $variables{$var} = $value; + + $self->pointed_hint('unnecessary-source-date-epoch-assignment', + $pointer) + if $var eq 'SOURCE_DATE_EPOCH' + && !$build_all->satisfies( + 'dpkg-dev:any (>= 1.18.8) | debhelper:any (>= 10.10)'); + } + + # Keep track of whether this portion of debian/rules may be optional + if ($line =~ /^ifn?(?:eq|def)\s(.*)/) { + push(@conditionals, $1); + $maybe_skipping++; + + } elsif ($line =~ /^endif\s/) { + $maybe_skipping--; + } + + unless ($maybe_skipping) { + + for my $prerequisite (keys %GLOBAL_DEPENDS) { + + my @patterns = @{ $GLOBAL_DEPENDS{$prerequisite} }; + + push(@needed, $prerequisite) + if any { $line =~ $_ } @patterns; + } + + for my $prerequisite (keys %GLOBAL_CLEAN_DEPENDS) { + + my @patterns = @{ $GLOBAL_CLEAN_DEPENDS{$prerequisite} }; + + if (any { $line =~ $_ } @patterns) { + + push(@needed, $prerequisite); + push(@needed_clean, $prerequisite); + } + } + } + + # Listing a rule as a dependency of .PHONY is sufficient to make it + # present for the purposes of GNU make and therefore the Policy + # requirement. + if ($line =~ /^(?:[^:]+\s)?\.PHONY(?:\s[^:]+)?:(.+)/s) { + + my @targets = split($SPACE, $1); + for my $target (@targets) { + # Is it $(VAR) ? + if ($target =~ /^\$[\(\{]([^\)\}]++)[\)\}]$/) { + my $name = $1; + my $val = $variables{$name}; + if ($val) { + # we think we know what it will expand to - note + # we ought to "delay" it was a "=" variable rather + # than ":=" or "+=". + + # discards empty elements at end, effectively trimming right + for (split(/\s+/, $val)) { + $seen{$target}++ + if exists $TAG_FOR_POLICY_TARGET{$target}; + } + last; + } + # We don't know, so just mark the target as seen. + } + $seen{$target}++ + if exists $TAG_FOR_POLICY_TARGET{$target}; + } + + #.PHONY implies the rest will not match + next; + } + + if ( !$includes + && $line + =~ /dpkg-parsechangelog.*(?:Source|Version|Date|Timestamp)/s) { + + $self->pointed_hint('debian-rules-parses-dpkg-parsechangelog', + $pointer); + } + + if ($line !~ /^ifn?(?:eq|def)\s/ && $line =~ /^([^\s:][^:]*):+(.*)/s) { + my ($target_names, $target_dependencies) = ($1, $2); + @current_targets = split $SPACE, $target_names; + + my @quoted = map { quotemeta } split($SPACE, $target_dependencies); + s/\\\$\\\([^\):]+\\:([^=]+)\\=([^\)]+)\1\\\)/$2.*/g for @quoted; + + my @depends = map { qr/^$_$/ } @quoted; + + for my $target (@current_targets) { + $overridden{$1} = $position if $target =~ m/override_(.+)/; + if ($target =~ /%/) { + my $pattern = quotemeta $target; + $pattern =~ s/\\%/.*/g; + for my $rulebypolicy (keys %TAG_FOR_POLICY_TARGET) { + $seen{$rulebypolicy}++ if $rulebypolicy =~ m/$pattern/; + } + } else { + # Is it $(VAR) ? + if ($target =~ m/^\$[\(\{]([^\)\}]++)[\)\}]$/) { + my $name = $1; + my $val = $variables{$name}; + if ($val) { + # we think we know what it will expand to - note + # we ought to "delay" it was a "=" variable rather + # than ":=" or "+=". + local $_ = undef; + + # discards empty elements at end, effectively trimming right + for (split(/\s+/, $val)) { + $seen{$_}++ + if exists $TAG_FOR_POLICY_TARGET{$_}; + } + last; + } + # We don't know, so just mark the target as seen. + } + $seen{$target}++ if exists $TAG_FOR_POLICY_TARGET{$target}; + } + if (any { $target =~ /$_/ } @arch_rules) { + push(@arch_rules, @depends); + } + } + undef %debhelper_group; + + } elsif ($line =~ /^define /) { + # We don't want to think the body of the define is part of + # the previous rule or we'll get false positives on tags + # like binary-arch-rules-but-pkg-is-arch-indep. Treat a + # define as the end of the current rule, although that + # isn't very accurate either. + @current_targets = (); + + } else { + # If we have non-empty, non-comment lines, store them for + # all current targets and check whether debhelper programs + # are called in a reasonable order. + if ($line =~ /^\s+[^\#]/) { + my ($arch, $indep) = (0, 0); + for my $target (@current_targets) { + $rules_per_target{$target} ||= []; + push(@{$rules_per_target{$target}}, $line); + + $arch = 1 + if any { $target =~ /$_/ } @arch_rules; + + $indep = 1 + if any { $target eq $_ } @indep_rules; + + $indep = 1 + if $target eq $PERCENT; + + $indep = 1 + if $target =~ /^override_/; + } + + if (!$maybe_skipping && ($arch || $indep)) { + + for my $prerequisite (keys %RULE_CLEAN_DEPENDS) { + + my @patterns = @{ $RULE_CLEAN_DEPENDS{$prerequisite} }; + + if (any { $line =~ $_ } @patterns) { + + push(@needed, $prerequisite); + push(@needed_clean, $prerequisite) + if $arch; + } + } + } + + if ($line =~ /^\s+(dh_\S+)\b/ && $debhelper_order{$1}) { + my $command = $1; + my ($package) = ($line =~ /\s(?:-p|--package=)(\S+)/); + $package ||= $EMPTY; + my $group = $debhelper_order{$command}; + $debhelper_group{$package} ||= 0; + + if ($group < $debhelper_group{$package}) { + + $self->pointed_hint( + 'debian-rules-calls-debhelper-in-odd-order', + $pointer, $command); + + } else { + $debhelper_group{$package} = $group; + } + } + } + } + + } continue { + ++$position; + } + + my @missing_targets; + @missing_targets = grep { !$seen{$_} } keys %TAG_FOR_POLICY_TARGET + unless $includes; + + $self->pointed_hint($TAG_FOR_POLICY_TARGET{$_}, $rules->pointer, $_) + for @missing_targets; + + # Make sure we have no content for binary-arch if we are arch-indep: + $rules_per_target{'binary-arch'} ||= []; + if ($architecture eq 'all' && scalar @{$rules_per_target{'binary-arch'}}) { + + my $nonempty = 0; + for my $rule (@{$rules_per_target{'binary-arch'}}) { + # dh binary-arch is actually a no-op if there is no + # Architecture: any package in the control file + $nonempty = 1 + unless $rule =~ /^\s*dh\s+(?:binary-arch|\$\@)/; + } + + $self->pointed_hint('binary-arch-rules-but-pkg-is-arch-indep', + $rules->pointer) + if $nonempty; + } + + for my $cmd (qw(dh_clean dh_fixperms)) { + for my $suffix ($EMPTY, '-indep', '-arch') { + + my $memorized_position = $overridden{"$cmd$suffix"}; + next + unless defined $memorized_position; + + $self->pointed_hint( + "override_$cmd-does-not-call-$cmd", + $rules->pointer($memorized_position) + ) + if none { m/^\t\s*-?($cmd\b|\$\(overridden_command\))/ } + @{$rules_per_target{"override_$cmd$suffix"}}; + } + } + + if (my $memorized_position = $overridden{'dh_auto_test'}) { + + my @rules = grep { + !m{^\t\s*[\:\[]} + && !m{^\s*$} + && !m{\bdh_auto_test\b} + && ! +m{^\t\s*[-@]?(?:(?:/usr)?/bin/)?(?:cp|chmod|echo|ln|mv|mkdir|rm|test|true)} + } @{$rules_per_target{'override_dh_auto_test'}}; + + $self->pointed_hint( + 'override_dh_auto_test-does-not-check-DEB_BUILD_OPTIONS', + $rules->pointer($memorized_position)) + if @rules and none { m/(DEB_BUILD_OPTIONS|nocheck)/ } @conditionals; + } + + $self->pointed_hint( + 'debian-rules-contains-unnecessary-get-orig-source-target', + $rules->pointer) + if any { m/^\s+uscan\b/ } @{$rules_per_target{'get-orig-source'}}; + + my @clean_in_indep + = grep { $build_indep->satisfies($_) } uniq @needed_clean; + $self->pointed_hint( + 'missing-build-depends-for-clean-target-in-debian-rules', + $rules->pointer, "(does not satisfy $_)") + for @clean_in_indep; + + # another check complains when debhelper is missing from d/rules + my $combined_lc = List::Compare->new(\@needed, ['debhelper:any']); + + my @still_missing + = grep { !$build_all_norestriction->satisfies($_) } + $combined_lc->get_Lonly; + + $self->pointed_hint('rules-require-build-prerequisite', + $rules->pointer, "(does not satisfy $_)") + for @still_missing; + + $self->pointed_hint('debian-rules-should-not-set-CFLAGS-from-noopt', + $rules->pointer) + if $contents + =~ m{^ ifn?eq \s+ [(] , \$ [(] findstring \s+ noopt , \$ [(] DEB_BUILD_OPTIONS [)] [)] [)] \n+ + \t+ CFLAGS \s+ \+ = \s+ -O[02] \n+ + else \n+ + \t+ CFLAGS \s+ \+ = \s+ -O[02] \n+ + endif $}xsm; + + return; +} + +1; + +# Local Variables: +# indent-tabs-mode: nil +# cperl-indent-level: 4 +# End: +# vim: syntax=perl sw=4 sts=4 sr et |