#!/usr/bin/perl -w =head1 NAME dh_pycentral - use the python-central framework to handle Python modules and extensions =cut use strict; use File::Find; use Debian::Debhelper::Dh_Lib; =head1 SYNOPSIS B [S>] [B<-n>] [B<-X>I] [B<-V> I] [S>] =head1 DESCRIPTION dh_pycentral is a debhelper program that will scan your package, detect public Python modules and move them in /usr/share/pycentral so that python-central can byte-compile those for all supported Python versions. Extensions are kept into the original installation location. Moving the files to the pycentral location can be disabled by setting the environment varibale DH_PYCENTRAL to a string containing the string B. You must have filled the XS-Python-Version header to indicate the set of python versions that are going to be supported. dh_pycentral expects the XB-Python-Version for each binary package it is supposed to work on. dh_pycentral will also generate substitution variables: the ${python:Provides} variable will contain versioned provides of the package (if the package's name starts with "python-"). A python-foo package could provide "python2.3-foo" and "python2.4-foo" at the same time. Python extensions have to provide those whereas it's only option for pure python modules. The ${python:Versions} variable should be used to provide the required XB-Python-Version field listing the python versions supported by the package. =head1 OPTIONS =over 4 =item I If your package installs python modules in non-standard directories, you can make dh_pycentral check those directories by passing their names on the command line. By default, it will check /usr/lib/$PACKAGE, /usr/share/$PACKAGE, /usr/lib/games/$PACKAGE, /usr/share/games/$PACKAGE and /usr/lib/python?.?/site-packages. Note: only /usr/lib/python?.?/site-packages and the extra names on the command line are searched for binary (.so) modules. =item B<-V> I If the .py files your package ships are meant to be used by a specific pythonX.Y version, you can use this option to specify the desired version, such as 2.3. Do not use if you ship modules in /usr/lib/site-python. With the new policy, this option is mostly deprecated. Use the XS-Python-Field to indicate that you're using a specific python version. =item B<-n>, B<--noscripts> Do not modify postinst/postrm scripts. =item B<-X>I, B<--exclude=>I Exclude files that contain "item" anywhere in their filename from being taken into account to generate the python dependency. You may use this option multiple times to build up a list of things to exclude. =back =head1 CONFORMS TO Python policy, version 0.4.1 (2006-06-20) =cut init(); my $python = 'python'; # The current python major version my $python_major; my $python_version = `$python -V 2>&1`; if (! defined $python_version || $python_version eq "") { error("Python is not installed, aborting. (Probably forgot to Build-Depend on python.)"); } elsif ($python_version =~ m/^Python\s+(\d+)\.(\d+)(\.\d+)*/) { $python_version = "$1.$2" ; $python_major = $1 ; } else { error("Unable to parse python version out of \"$python_version\"."); } # The next python version my $python_nextversion = next_minor_version($python_version); my $python_nextmajor = $python_major + 1; my @python_allversions = ('1.5','2.1','2.2','2.3','2.4','2.5','2.6'); foreach (@python_allversions) { s/^/python/; } # Check for -V my $usepython = "python$python_version"; if($dh{V_FLAG_SET}) { $usepython = $dh{V_FLAG}; $usepython =~ s/^/python/; if (! grep { $_ eq $usepython } @python_allversions) { error("Unknown python version $dh{V_FLAG}"); } } # Cleaning the paths given on the command line foreach (@ARGV) { s#/$##; s#^/##; } # Check the compatibilty mode to use my $pyversions_field = ""; my $python_header = ""; { local $/ = ""; # Grab until empty line open(CONTROL, "debian/control"); # Can't fail, dh_testdir has already been called my $source = ; close(CONTROL); if ($source =~ m/^XS-Python-Version: \s*(.*)$/m) { $python_header = $1; chomp($python_header); $pyversions_field = convert_python_header($python_header); } } # pyversions describes the list of python versions that this package can # work with if (-e "debian/pyversions") { open(PYVERS, "debian/pyversions") || error("Can't open debian/pyversions: $!"); $pyversions_field = ; chomp($pyversions_field); close(PYVERS); } verbose_print("Pyversions field: $pyversions_field"); # Extract a list of officially supported Python versions my %officially_supported; if (-e "/usr/share/python/debian_defaults") { open(DEFAULTS, "/usr/share/python/debian_defaults") || error("Can't open /usr/share/python/debian_defaults: $!"); foreach () { if (/^supported-versions\s*=\s*(.*)$/) { my $supported = $1; foreach my $version (split(/\s+/, $supported)) { if ($version =~ /python([\d\.]+)/) { $officially_supported{$1} = 1; } } last; } } close(DEFAULTS); } # dependency types use constant PROGRAM => 1; use constant PY_PRIVATE_MODULE => 2; use constant PY_PUBLIC_MODULE => 4; use constant PY_OFFICIAL_MODULE => 8; use constant PY_UNKNOWN_MODULE => 16; use constant SO_PRIVATE_MODULE => 32; use constant SO_PUBLIC_MODULE => 64; use constant SO_OFFICIAL_MODULE => 128; use constant SO_UNKNOWN_MODULE => 256; foreach my $package (@{$dh{DOPACKAGES}}) { my $tmp = tmpdir($package); # Move *.py files if needed doit("pycentral debhelper $package $tmp"); # Check that we have *.py files! my $found = 0; find sub { return if $File::Find::dir =~ m|^\Q$tmp\E/usr/share/doc/|; return unless -f and /\.py$/; $found++; }, $tmp; if ($found or -d "$tmp/usr/share/pycentral") { addsubstvar($package, "python:Depends", "python-central", ">= 0.5.8"); if (! $dh{NOSCRIPTS}) { autoscript($package,"postinst","postinst-pycentral","s%#PACKAGE#%$package%"); autoscript($package,"prerm","prerm-pycentral","s%#PACKAGE#%$package%"); } } # Here we're doing what dh_python used to do my @dirs = ("usr/lib/$package", "usr/share/$package", "usr/lib/games/$package", "usr/share/games/$package", @ARGV ); my @dirs_so = (@ARGV); my $dep_on_python = 0; my $strong_dep = 0; # Fail early if the package use usr/lib/site-python if (-d "$tmp/usr/lib/site-python") { error("The package $package puts files in /usr/lib/site-python: forbidden by policy"); } @dirs = grep -d, map "$tmp/$_", @dirs; @dirs_so = grep -d, map "$tmp/$_", @dirs_so; my $deps = 0; my %verdeps = (); foreach (@python_allversions) { $verdeps{$_} = 0; } # Global scan my $private_pydirs_regex = join('|', map { "\Q$_\E" } @dirs); my $private_sodirs_regex = join('|', map { "\Q$_\E" } @dirs_so); my %private_dirs_list; my %pyversions_found; find sub { return unless -f; # See if we were asked to exclude this file. # Note that we have to test on the full filename, including directory. my $fn="$File::Find::dir/$_"; if (excludefile($fn)) { verbose_print("Ignoring $fn"); return; } # Find scripts if (-x or /\.py$/) { local *F; return unless open F, $_; if (read F, local $_, 32 and m%^#!\s*/usr/bin/(env\s+)?(python(\d+\.\d+)?)\s%) { verbose_print("Found program using $2: $fn"); if ( "python" eq $2 ) { $deps |= PROGRAM; } elsif(defined $verdeps{$2}) { $verdeps{$2} |= PROGRAM; } } close F; } # Continue only with .py or .so return unless (/\.py$/ or /\.so$/); # Remove any byte-compiled file doit(("rm","-f",$_."c",$_."o")) if /\.py$/; # Classify the file in the right category if (/\.py$/ and $private_pydirs_regex and $fn =~ m/(?:$private_pydirs_regex)/) { # Private python module verbose_print("Found private module: $fn"); my $dir; foreach $dir (@dirs) { if ($fn =~ m/\Q$dir\E/) { $dir =~ s/^$tmp//; verbose_print("Memorizing dir to byte-compile: $dir"); $private_dirs_list{"$dir"} = 1; } } if ($dh{V_FLAG_SET}) { $verdeps{$usepython} |= PY_PRIVATE_MODULE; } else { $deps |= PY_PRIVATE_MODULE; } } elsif (/\.so$/ and $private_sodirs_regex and $fn =~ m/(?:$private_sodirs_regex)/) { # Private python extension verbose_print("Found private extension: $fn"); if ($dh{V_FLAG_SET}) { $verdeps{$usepython} |= SO_PRIVATE_MODULE; } else { $deps |= SO_PRIVATE_MODULE; } } elsif ($fn =~ m|$tmp/usr/lib/python([\d\.]+)/site-packages/|) { $pyversions_found{$1} = 1; my $v = $1; if (/\.py$/) { verbose_print("Found public module: $fn"); $verdeps{"python$v"} |= PY_PUBLIC_MODULE; } else { verbose_print("Found public extension: $fn"); $verdeps{"python$v"} |= SO_PUBLIC_MODULE; } } elsif ($fn =~ m|$tmp/usr/lib/python([\d\.]+)/|) { $pyversions_found{$1} = 1; my $v = $1; if (/\.py$/) { verbose_print("Found official module: $fn"); $verdeps{"python$v"} |= PY_OFFICIAL_MODULE; } else { verbose_print("Found official extension: $fn"); $verdeps{"python$v"} |= SO_OFFICIAL_MODULE; } } elsif ($fn =~ m|$tmp/usr/lib/python-support/[^/]+/python([\d\.]+)/|) { $pyversions_found{$1} = 1; my $v = $1; if (/\.py$/) { verbose_print("Found public module: $fn"); $verdeps{"python$v"} |= PY_PUBLIC_MODULE; } else { verbose_print("Found public extension: $fn"); $verdeps{"python$v"} |= SO_PUBLIC_MODULE; } } elsif ($fn =~ m{$tmp(?:/usr/share/pycentral/|/usr/share/python-support/)}) { if (/\.py$/) { verbose_print("Found public module: $fn"); $deps |= PY_PUBLIC_MODULE; } # No extensions here } elsif ($fn =~ m|$tmp/usr/lib/python([\d\.]+)/site-packages/|) { } elsif ($fn =~ m|$tmp/usr/share/doc/|) { # Ignore .py files in doc directory } else { # Unknown pyfiles if (/\.py$/) { verbose_print("Found unclassified module: $fn"); if ($dh{V_FLAG_SET}) { $verdeps{$usepython} |= PY_UNKNOWN_MODULE; } else { $deps |= PY_UNKNOWN_MODULE; } } else { verbose_print("Found unclassified extension: $fn"); if ($dh{V_FLAG_SET}) { $verdeps{$usepython} |= SO_UNKNOWN_MODULE; } else { $deps |= SO_UNKNOWN_MODULE; } } } }, $tmp; # # NEW POLICY # # Generate the depends to accept all python # versions that this package effectively provides my ($min_version, $max_version, @versions) = analyze_pyversions($pyversions_field); my $stop_version = ""; $stop_version = next_minor_version($max_version) if $max_version; my $versions_field = ""; verbose_print("Pyversions analysis gives: min=$min_version, max=$max_version (@versions)"); # Common dependency handling foreach my $pyver (keys %verdeps) { # Always add pythonX.Y dependency if a script uses that interpreter if ($verdeps{$pyver} & PROGRAM) { addsubstvar($package, "python:Depends", $pyver); } # Always add pythonX.Y dependency if some private modules are # byte-compiled with it (or if extensions are # byte-compiled with it) if ($verdeps{$pyver} & (PY_PRIVATE_MODULE|SO_PRIVATE_MODULE)) { addsubstvar($package, "python:Depends", $pyver); unless ($versions_field) { $versions_field = $pyver; $versions_field =~ s/^python//; } } } # Reset again, analysis using new policy follows $dep_on_python = 0; # Private extensions, must be rebuilt for each python version if ($deps & SO_PRIVATE_MODULE) { if (($dh{V_FLAG_SET} and ($usepython eq "python$python_version")) or (($min_version eq $python_version) and ($min_version eq $max_version)) ) { # Depend on python only if the version # used to build is the currently supported one $dep_on_python++; } # Packages using a private extension can only support one # version: if versions_field is already set that's because # we're using -V and we should respect that, otherwise try # the best guess: unless($versions_field) { if ($min_version and ($min_version eq $max_version)) { # Only one version supported, use it $versions_field = $min_version; } elsif (compare_version($min_version, $python_version) > 0) { # The current version is unsupported, use the min version instead $versions_field = $min_version; } else { # Use the current version by default $versions_field = $python_version; $min_version = $python_version; } } $stop_version = next_minor_version($versions_field); } # Private modules if ($deps & PY_PRIVATE_MODULE) { # Package with private modules can only support one version at a time # (even if the modules can be automatically byte-compiled for any new # python version). unless ($versions_field) { # Unless min/max are the same we put "current" if ($min_version and ($min_version eq $max_version)) { $versions_field = $min_version; } elsif (compare_version($min_version, $python_version) > 0) { # The current version is unsupported, use the min version instead $versions_field = $min_version; } else { # Use the current version by default $versions_field = "current"; } } } # Python scripts & public modules if ($deps & (PROGRAM|PY_PUBLIC_MODULE)) { $dep_on_python++; } # Public extensions if (scalar keys %pyversions_found) { # Extensions will always trigger this (as well as public # modules not handled by python-support/python-central) $dep_on_python++; if (scalar grep { $python_version eq $_ } keys %pyversions_found) { # Current versions is supported by the package # It's safe to depends on the corresponding # interval of python versions $min_version = min(keys %pyversions_found); unless ($stop_version) { $max_version = max(keys %pyversions_found); $stop_version = next_minor_version($max_version); } } else { # Current version is not supported by the package if ($min_version and ($min_version eq $max_version)) { # If we support only one non-standard python # version, the depend on that version and not on python $dep_on_python--; if (! $dep_on_python) { addsubstvar($package, "python:Depends", "python$min_version"); } } } # Generate the Python-Version field foreach (keys %pyversions_found) { addsubstvar($package, "python:Versions", $_); } # Generate provides for the python2.X-foo packages that we emulate if ($package =~ /^python-/) { foreach (keys %pyversions_found) { my $virtual = $package; $virtual =~ s/^python-/$python$_-/; addsubstvar($package, "python:Provides", $virtual); } } } else { # Still try to describe the versions that the package support $versions_field = $python_header unless $versions_field; $versions_field = "all" unless $versions_field; addsubstvar($package, "python:Versions", $versions_field); # Generate provides for all python versions supported if ($package =~ /^python-/) { foreach (grep { $officially_supported{$_} } @versions) { my $virtual = $package; $virtual =~ s/^python-/$python$_-/; addsubstvar($package, "python:Provides", $virtual); } } } if ($dep_on_python) { # At least a script has been detected if ($min_version) { if (compare_version($min_version, $python_version) <= 0 ) { # Min-version is less or equal to current version addsubstvar($package, "python:Depends", $python, ">= $min_version"); } else { # Min version is greater to current version # Supposition: new stuff working only with the latest python, # depend on that specific python version addsubstvar($package, "python:Depends", "python (>= $min_version) | python$min_version"); } } # If a stronger dependency is needed if ($stop_version) { if (compare_version($stop_version, $python_version) > 0 ) { # Stop version is strictly bigger than current version addsubstvar($package, "python:Depends", $python, "<< $stop_version"); } else { # Only works with old versions, # package is mostly deprecated, # no need for a python dependency } } # Let's depend on python anyway addsubstvar($package, "python:Depends", $python) unless ($min_version or $stop_version); } } sub next_minor_version { my $version = shift; # Handles 2.10 -> 2.11 gracefully my @items = split(/\./, $version); $items[1] += 1; $version = join(".", @items); return $version; } sub compare_version { my ($a, $b) = @_; my @A = split(/\./, $a); my @B = split(/\./, $b); my $diff = 0; for (my $i = 0; $i <= $#A; $i++) { $diff = $A[$i] - $B[$i]; return $diff if $diff; } # They are the same return 0; } sub max { my $max = shift; foreach (@_) { $max = $_ if (compare_version($_, $max) > 0); } return $max; } sub min { my $min = shift; foreach (@_) { $min = $_ if (compare_version($_, $min) < 0); } return $min; } # Extract min, max and list of versions from # a string like this "-1.2,1.4-1.6,2.0,2.3-" sub analyze_pyversions { my $field = shift; my ($min, $max, @versions); my @all_versions = ("0.0"); foreach (@python_allversions) { if (m/python([\d\.]+)/) { push @all_versions, $1; } } push @all_versions, "100.0"; foreach my $range (split /,/, $field) { if ($range =~ /^([\d\.]+)?-([\d\.]+)?$/) { $min = defined($1) && $1 ? $1 : "0.0"; $max = defined($2) && $2 ? $2 : "100.0"; push @versions, grep { compare_version($_, $min) >= 0 and compare_version($_, $max) <= 0 } @all_versions; } else { push @versions, $range if (grep { $range eq $_ } @all_versions); } } $min = min(@versions); $max = max(@versions); $min = "" if (!defined($min) or $min eq "0.0"); $max = "" if (!defined($max) or $max eq "100.0"); @versions = grep { $_ ne "0.0" and $_ ne "100.0" } @versions; return ($min, $max, @versions); } # Convert the Python-Version field in a standardized "pyversions" field sub convert_python_header { my $header = shift; my %pyversions_expected; my %pyversions_additional; my $use_additional = 0; my @all_versions = (); foreach (@python_allversions) { if (m/python([\d\.]+)/) { $pyversions_additional{$1} = 1; push @all_versions, $1; } } # Add two fakes minima, maxima to check if we have limits $pyversions_additional{"0.0"} = 1; $pyversions_additional{"100.0"} = 1; # Compute the list of versions that are supported foreach my $check (split(/,\s*/, $header)) { if ($check =~ /^=?\s*([\d\.]+)/) { # Add any fixed version $pyversions_expected{$1} = 1 if (grep { $_ eq $1 } @all_versions); next; } # Deactivate versions which do not match the other # criteria if ($check =~ /^<<\s*([\d\.]+)/) { my $v = $1; $use_additional = 1; foreach (keys %pyversions_additional) { $pyversions_additional{$_} = 0 unless (compare_version($_, $v) < 0); } } elsif ($check =~ /^<=\s*([\d\.]+)/) { my $v = $1; $use_additional = 1; foreach (keys %pyversions_additional) { $pyversions_additional{$_} = 0 unless (compare_version($_, $v) <= 0); } } elsif ($check =~ /^>=\s*([\d\.]+)/) { my $v = $1; $use_additional = 1; foreach (keys %pyversions_additional) { $pyversions_additional{$_} = 0 unless (compare_version($_, $v) >= 0); } } } if ($use_additional) { foreach (grep { $pyversions_additional{$_} } keys %pyversions_additional) { $pyversions_expected{$_} = 1; } } my @supported = sort { compare_version($a, $b) } grep { $pyversions_expected{$_} == 1 } keys %pyversions_expected; verbose_print("List of versions supported according to XS-Python-Version: @supported"); # Generate the corresponding information in standardized "pyversions" format # XXX: I go to great length to generate the shortest pyversions # syntax but it may not be necessary for the usage that we make of it. my ($result, $index, $range_is_open, $last_real_version) = ("", 0, 0, ""); foreach (@supported) { if ($_ eq "0.0") { $result .= "-"; # No lower limit $range_is_open = 1; } elsif ($_ eq "100.0") { $result .= "-" unless $range_is_open; # No upper limit $range_is_open = 0; # Proper end } else { $last_real_version = $_; if ($range_is_open) { # Range is open if ($index <= $#all_versions and $all_versions[$index] eq $_) { # This version still in the range } else { # Previous version ended the range $result .= $all_versions[$index-1] . ",$_"; $range_is_open = 0; } # Find the current version in all_versions while ($index <= $#all_versions) { last if ($all_versions[$index] eq $_); $index++; } } else { # There's no range yet if ($result) { if ($index <= $#all_versions and $all_versions[$index] eq $_) { # This version still match, we can start a range $result .= "-"; $range_is_open = 1; } else { # Next version doesn't match, no range but a list $result .= ",$_"; } } else { # Empty field, start with something! $result = "$_"; } while ($index <= $#all_versions) { last if ($all_versions[$index] eq $_); $index++; } } $index++; } } if ($range_is_open) { # Close the range properly $result .= $last_real_version; } return $result; } =head1 SEE ALSO L This program is a part of python-central but is made to work with debhelper. =head1 AUTHORS Raphael Hertzog Also includes bits of the old dh_python written by Josselin Mouette who used many ideas from Brendan O'Dea . =cut