Edit file File name : dh_sphinxdoc Content :#!/usr/bin/perl # Copyright © 2011 Jakub Wilk <jwilk@debian.org> # © 2014-2018 Dmitry Shachnev <mitya57@debian.org> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =head1 NAME dh_sphinxdoc - helps with packaging Sphinx-generated documentation =head1 SYNOPSIS dh_sphinxdoc [S<I<debhelper options>>] [B<-X>I<item>] [I<directory>...] =head1 DESCRIPTION B<dh_sphinxdoc> is a debhelper program that prepares Sphinx-generated documentation for inclusion in a Debian package. More specifically: =over 4 =item * It checks if all the files referenced by F<searchindex.js> exist. =item * It replaces known F<*.js> files with symlinks to F</usr/share/javascript/sphinxdoc/> and generates B<${sphinxdoc:Depends}> substitution variable. =item * If the Sphinx RTD theme is used, it replaces known files from this theme with symlinks to F</usr/share/sphinx_rtd_theme/>, and adds B<sphinx-rtd-theme-common> to B<${sphinxdoc:Depends}>. =item * It provides a B<${sphinxdoc:Built-Using}> substitution variable, for tracking files which could not be symlinked. Examples of such files are F<*.js> and F<*.css> files that are generated from corresponding F<*.js_t> and F<*.css_t> templates, and can vary depending on the used theme options (for instance, F<basic.css> file is generated from F<basic.css_t> and is included in almost every Sphinx-generated documentation). Currently, this variable will contain B<sphinx> and, if the default theme is used, B<alabaster>, with their versions (other themes are not supported). =item * It removes the F<.doctrees> directory. =item * It removes the F<.buildinfo> file. =item * It removes the F<websupport.js> file. =back Note that B<dh_sphinxdoc> does not build the documentation, it only performs the above operations when the documentation is already installed into the package build path. To build the docs, please use L<sphinx-build(1)> command or B<python3 -m sphinx> syntax. You can pass B<--with sphinxdoc> to L<dh(1)> to make it automatically call B<dh_sphinxdoc> after B<dh_installdocs>. =head1 OPTIONS =over 4 =item I<directory> By default, B<dh_sphinxdoc> scans your package looking for directories looking like they contain Sphinx-generated documentation. However, if you explicitly provide one or more directories, only they will be processed. If documentation is not found at I<directory>, an error is raised. =item B<-X>I<item>, B<--exclude=>I<item> Exclude files that contain I<item> anywhere in their filename from being symlinked, removed or checked for existence. =back =head1 BUGS Symlinking translations.js is not supported. =cut use strict; use warnings; use Digest::MD5; use File::Find; use Debian::Debhelper::Dh_Lib; my %packaged_js = (); my @cruft_js = qw(websupport.js); sub md5($) { my ($filename) = @_; my $md5 = Digest::MD5->new; open(F, '<', $filename) or error("cannot open $filename"); $md5->addfile(*F); close(F); return $md5->digest; } sub load_packaged_js() { my %versions = (); my $root = 'debian/libjs-sphinxdoc'; # It's tempting to use # tmpdir('libjs-sphinxdoc') here, but it would break if the user passed # --tmpdir to the command. $root = '' unless -d $root; my $path = "$root/usr/share/javascript/sphinxdoc"; open(F, '<', "$path/index") or error("cannot open $path/index"); while (<F>) { chomp; next if /^(#|\s*$)/; my ($js, $minver) = split(/\s+/, $_, 2); unless (defined($minver)) { $js =~ m{^([0-9.]+)/} or error("syntax error in $path/index"); $minver = $1; } $versions{$js} = $minver; } close(F); find({ wanted => sub { my $js = $_; my ($jsbase, $jsname) = m{([0-9.]+/(\S+[.]js))$} or return; my $version = $versions{$jsbase}; defined($version) or error("$jsbase is not in the index; is it up-to-date?"); delete $versions{$jsbase}; my $md5; if (-l $js) { # Follow the symlink, but only if points *outside* our own directory. my $js_target = readlink($js); $js_target =~ m{^(/|\Q../../\E)} or return; unless ($js_target =~ m{^/}) { $js_target = "$js/../$js_target"; while ($js_target =~ s{[^./][^/]+/[.][.]/}{}) {}; } $md5 = md5($js_target); } else { $js =~ s{^\Q$root\E}{} unless -f $js; $md5 = md5($js); } $js =~ s{^\Q$root\E}{}; my $data = [$js, "libjs-sphinxdoc (>= $version)"]; $packaged_js{$md5} = $data; $packaged_js{$jsname} = $data; }, no_chdir => 1 }, $path); map { error("$path/$_ is missing") } keys(%versions); } sub looks_like_sphinx_doc($) { my ($path) = @_; return 0 unless -f "$path/searchindex.js"; return 0 unless -f "$path/search.html"; return 1; } sub looks_like_sphinx_singlehtml_doc($) { my ($path) = @_; return 0 unless -d "$path/_static"; return 0 if -f "$path/searchindex.js"; # There should be exactly one HTML file in singlehtml build. my @html_files = glob("$path/*.html"); my @sphinx_html_files; foreach my $html_file (@html_files) { open(my $fh, '<', $html_file) or error("cannot open $html_file"); while (my $line = <$fh>) { if ($line =~ /<script type="text\/javascript" src="_static\/doctools.js">/) { push @sphinx_html_files, $html_file; last; } } } return 0 if @sphinx_html_files != 1; return $sphinx_html_files[0]; } sub sanity_check($$) { local $/; my ($path, $singlehtml_file) = @_; my $searchfn; my $index; if ($singlehtml_file) { # There is no search.html in singlehtml build, so we take the main HTML # file for sanity checking and retrieving JS files. $searchfn = $singlehtml_file; } else { my $indexfn = "$path/searchindex.js"; open(F, '<', $indexfn) or error("cannot open $indexfn"); $index = <F>; close(F); $index =~ m{^Search[.]setIndex[(].*?filenames:\["(.*?)"\].*[)]$} or error("$indexfn doesn't look like a Sphinx search index"); $index = $1; $searchfn = "$path/search.html"; } open(F, '<', $searchfn) or error("cannot open $searchfn"); my $search = <F>; close F; $search =~ s/<!--.*?-->//g; # strip comments my %js = (); grep { s/[?#].*//; $js{$_} = 1 unless m/^[a-z][a-z0-9.+-]*:/i or excludefile("$path/$_"); } $search =~ m{<script type="text/javascript" src="([^"]++)"></script>}g; my $documentation_options; if ($search =~ "_static/documentation_options.js") { my $documentation_options_fn = "$path/_static/documentation_options.js"; open(my $fh, '<', $documentation_options_fn) or error("cannot open $documentation_options_fn"); $documentation_options = <$fh>; close $fh; } else { $documentation_options = $search; } my $loads_searchindex = $search =~ m{<script type="text/javascript" src="[^"]?searchindex.js" defer>}; unless ($loads_searchindex) { # old style, used before Sphinx 2.0 $loads_searchindex = $search =~ m/\QjQuery(function() { Search.loadIndex("\E/; } my ($has_source) = $documentation_options =~ m{HAS_SOURCE:\s*(true|false)}; my ($sourcelink_suffix) = $documentation_options =~ m{SOURCELINK_SUFFIX:\s*'([^']*)'}; $sourcelink_suffix = ".txt" unless defined $sourcelink_suffix; my $url_root; if ($documentation_options =~ /\QURL_ROOT: document.getElementById("documentation_options")\E/) { ($url_root) = $search =~ m{data-url_root="([^"]*)"}; } else { ($url_root) = $documentation_options =~ m{URL_ROOT:\s*'([^']*)'}; } %js or error("$searchfn does not include any JavaScript code"); $singlehtml_file or $loads_searchindex or error("$searchfn does not load searchindex.js"); defined $has_source or error("DOCUMENTATION_OPTIONS does not define HAS_SOURCE"); defined $url_root or error("DOCUMENTATION_OPTIONS does not define URL_ROOT"); $has_source = $has_source eq 'true'; $url_root =~ m{^([a-z]+:/)?/} and error("URL_ROOT in $searchfn is not relative"); for my $js (keys(%js)) { -f "$path/$js" or -l "$path/$js" or error("$path/$js is missing"); } unless ($singlehtml_file) { for my $page (split(/","/, $index)) { # Append sourcelink_suffix if the page name does not already end with it. (my $sourcepage = $page) =~ s/(?<!$sourcelink_suffix)$/$sourcelink_suffix/; -f "$path/_sources/$sourcepage" or excludefile("$path/_sources/$sourcepage") or error("$path/_sources/$sourcepage is missing") if $has_source; # Get the page basename before appending .html. $page =~ s/\.[a-z]+$//; -f "$path/$page.html" or excludefile("$path/$page.html") or error("$path/$page.html is missing"); } } if (opendir(D, "$path/_static/")) { grep { $js{"_static/$_"} = 1 if /[.]js$/ and not excludefile("$path/_static/$_"); } readdir(D); closedir(D); } return keys(%js); } sub unknown_javascript($) { my ($js) = @_; my $message = "unknown JavaScript code: $js"; $js =~ s{.*/}{}; my $basic = grep { $_ eq $js } qw(searchtools.js doctools.js jquery.js underscore.js); my $cruft = grep { $_ eq $js } @cruft_js; my $basic_ignored = grep { $_ eq $js } qw(theme.js documentation_options.js language_data.js); if ($basic) { error("error: $message"); } elsif (not $cruft and not $basic_ignored) { warning("ignoring $message"); } } sub ln_sf($$) { my ($orig_target, $orig_source) = my ($target, $source) = @_; $source =~ s{^debian/[^/]++/+}{} or die; $target =~ s{^/++}{} or die; my @source = split(m{/++}, $source); my @target = split(m{/++}, $target); @source > 0 and @target > 0 or die; if ($source[0] eq $target[0]) { # Make the symlink relative, as per Policy 10.5. while (@source > 0 and @target > 0 and $source[0] eq $target[0]) { shift @source; shift @target; } $target = ('../' x $#source) . join('/', @target); } else { # Keep the symlink absolute, as per Policy 10.5. $target = $orig_target; } doit('ln', '-sf', $target, $orig_source); } sub fix_symlinks($@) { my %deps = (); my ($path, @js) = @_; for my $js (@js) { my $id = ''; if (-l "$path/$js") { my $symlink_target = readlink("$path/$js"); $symlink_target =~ m{/sphinxdoc/} and next; $symlink_target =~ m{/javascript/\w+/(\w+)([.](min|lite|pack))?[.]js$} and $id = "$1.js"; } elsif (-f "$path/$js") { $id = md5("$path/$js"); } if (exists $packaged_js{$id}) { my ($target, $dependency) = @{$packaged_js{$id}}; ln_sf($target, "$path/$js"); $deps{$dependency} = 1; } else { unknown_javascript("$path/$js"); } } return keys %deps; } sub drop_cruft($) { my ($path) = @_; my $doctrees = "$path/.doctrees"; my $buildinfo = "$path/.buildinfo"; if (-d $doctrees and not excludefile($doctrees)) { doit('rm', '-rf', $doctrees); } if (-f $buildinfo and not excludefile($buildinfo)) { doit('rm', '-f', $buildinfo); } foreach my $js (@cruft_js) { my $js = "$path/_static/$js"; if (-f $js and not excludefile($js)) { doit('rm', '-f', $js) if -f $js; } } } sub process_rtd($) { my ($path) = @_; my $theme_is_rtd = 0; if (-d "$path/_static/js" and -f "$path/_static/js/theme.js") { if (open(F, '<', "$path/_static/js/theme.js")) { while (my $line = <F>) { if (index($line, "window.SphinxRtdTheme") != -1) { $theme_is_rtd = 1; last; } } close(F); } } my @deps; my $target_dir = "/usr/share/sphinx_rtd_theme/static"; if ($theme_is_rtd and -d $target_dir) { find({ wanted => sub { return if -d; my $filename = $_; substr($filename, 0, 1 + length $target_dir) = ""; return unless -f "$path/_static/$filename"; ln_sf($_, "$path/_static/$filename"); }, no_chdir => 1 }, $target_dir); my $rtd_theme_version = get_installed_package_version("sphinx-rtd-theme-common"); $rtd_theme_version =~ s/-[^-]+$//; # Remove the Debian version suffix push @deps, "sphinx-rtd-theme-common (>= $rtd_theme_version)"; } return @deps; } sub get_installed_package_version($) { my ($package_name) = @_; return `dpkg-query -W -f '\${Version}' $package_name 2>/dev/null`; } sub list_built_using($) { my ($path) = @_; my @built_using; my $sphinx_version = get_installed_package_version("sphinx-common"); push @built_using, "sphinx (= $sphinx_version)"; if (-d "$path/_static" and -f "$path/_static/alabaster.css") { my $alabaster_version = get_installed_package_version("python3-alabaster"); unless ($alabaster_version) { $alabaster_version = get_installed_package_version("python-alabaster"); } if ($alabaster_version) { push @built_using, "alabaster (= $alabaster_version)"; } } return @built_using; } sub fix_sphinx_doc($$) { my ($package, $path) = @_; my $is_html = looks_like_sphinx_doc($path); my $singlehtml_file = looks_like_sphinx_singlehtml_doc($path); return 0 if not ($is_html or $singlehtml_file); my @js = sanity_check($path, $singlehtml_file); my @rtd_deps = process_rtd($path); my @deps = fix_symlinks($path, @js); my @built_using = list_built_using($path); drop_cruft($path); map { addsubstvar($package, "sphinxdoc:Depends", $_) } @deps; map { addsubstvar($package, "sphinxdoc:Depends", $_) } @rtd_deps; map { addsubstvar($package, "sphinxdoc:Built-Using", $_) } @built_using; return 1; } init(); load_packaged_js(); my @paths = @ARGV; @paths = (undef) unless @paths; foreach my $path (@paths) { my $done = 0; my @matching_packages; foreach my $package (@{$dh{DOPACKAGES}}) { my $pkgpath = tmpdir($package); if (defined $path) { next if -l $path; $pkgpath .= "/$path"; next unless -d $pkgpath; push @matching_packages, $package; $done += fix_sphinx_doc($package, $pkgpath); } else { $pkgpath .= '/usr/share/doc/'; next unless -d $pkgpath; find({ wanted => sub { return unless -d; return if -l; return if excludefile($_); $done += fix_sphinx_doc($package, $_); }, no_chdir => 1 }, $pkgpath); } } if ($done == 0) { if (defined $path) { if (!@matching_packages) { error("Path $path not found in any built package\n(searched in: @{$dh{DOPACKAGES}})"); } error("Sphinx documentation not found at $path\n(path found in packages: @matching_packages)"); } else { warning("Sphinx documentation not found"); } } } =head1 SEE ALSO L<debhelper(7)>, L<dh(1)>. This program is meant to be used together with debhelper. =head1 AUTHOR Jakub Wilk <jwilk@debian.org> =cut # vim:ts=4 sw=4 et Save