#!/usr/bin/perl # Copyright: 2018 Dmitry Shachnev # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =head1 NAME dh_mkdocs - helps with packaging MkDocs-generated documentation =head1 SYNOPSIS dh_mkdocs [S>] [B<-X>I] [B<-T>I | B<--theme-package> I] =head1 DESCRIPTION B is a helper program for packaging documentation generated by MkDocs. More specifically: =over 4 =item * It replaces static files with symlinks to their packaged versions if possible. For packages using non-standard themes, use the B<--theme-package> option, for example: dh_mkdocs --theme-package mkdocs-bootstrap B only symlinks non-empty files with identical content. =item * For the standard B and B themes, it replaces references to the highlight.js library from CDN with references to the packaged version of that library. =item * It adds the needed dependencies to B<${mkdocs:Depends}> substitution variable. Please add this variable to the B field of your documentation package. =back You can pass B<--with mkdocs> to L to make it automatically call B after B. =head1 OPTIONS =over 4 =item B<-X>I, B<--exclude> I Exclude files that contain I anywhere in their filename from being symlinked or edited. This option can be specified multiple times. =item B<-T>I, B<--theme-package> I Scan the specified additional I for files that can be used as symlink targets. This option can be specified multiple times. =back =cut use strict; use warnings; use Debian::Debhelper::Dh_Lib; use Digest::file qw(digest_file); use File::Find qw(find); use File::Spec; use IPC::Open2 qw(open2); my @theme_packages; my %package_dependencies; sub add_theme_package { my ($option, $value) = @_; push @theme_packages, $value; } init(options => { 'theme-package=s' => \&add_theme_package, 'T=s' => \&add_theme_package, }); my $mkdocs_directory_found = 0; foreach my $package (@{$dh{DOPACKAGES}}) { my $tmpdir = tmpdir($package); next unless -d $tmpdir; # in this hash, value 1 means versioned dependency, 0 means unversioned $package_dependencies{$package} = {}; find({ wanted => sub { return unless -d; return if -l; return if excludefile($_); return unless looks_like_mkdocs_directory($_); $mkdocs_directory_found = 1; fixup_highlightjs_usage($package, $_); if ($package ne 'mkdocs-doc') { symlink_static_files($package, $_); } write_substvars($package); }, no_chdir => 1 }, $tmpdir); } $mkdocs_directory_found or error('MkDocs directory not found'); sub looks_like_mkdocs_directory { my ($path) = @_; return 0 unless -f "$path/index.html"; return 0 unless -f "$path/sitemap.xml"; return 0 unless -f "$path/search/search_index.json"; return 1; } sub fixup_highlightjs_usage { my ($package, $path) = @_; my $cdn_hljs = '(?:https:)?//cdnjs.cloudflare.com/ajax/libs/highlight.js/[\d.]+'; my $pkg_hljs = 'file:///usr/share/javascript/highlight.js'; find({ wanted => sub { return unless -f; return unless /\.html$/s; my $filename = $_; open(my $in, '<', $_) or error("Failed to open $_ for read: $!"); open(my $out, '>', "$_.new") or error("Failed to open $_.new for write: $!"); while (<$in>) { if (m{$cdn_hljs/highlight.min.js}) { $package_dependencies{$package}{'libjs-highlight.js'} = 0; } s{$cdn_hljs/highlight.min.js}{$pkg_hljs/highlight.min.js}; s{$cdn_hljs/styles/github.min.css}{$pkg_hljs/styles/github.css}; print $out $_ unless m{$cdn_hljs/languages/}; } close($in); close($out); rename("$filename.new", $filename); }, no_chdir => 1 }, $path); } sub symlink_static_files { my ($package, $path) = @_; my %file_names_by_hash; my %package_names_by_hash; my @packages = ( 'fonts-lato', 'fonts-font-awesome', 'libjs-bootstrap4', 'libjs-jquery', 'libjs-lunr', 'libjs-modernizr', 'node-jquery', 'mkdocs', 'sphinx-rtd-theme-common' ); push @packages, @theme_packages; foreach my $package_name (@packages) { my $stdout; my $pid = open2($stdout, undef, 'dpkg-query', '--listfiles', $package_name); if ($pid) { while (<$stdout>) { chomp; if (-f && -s && ! -l) { my $digest = digest_file($_, 'SHA-1'); $file_names_by_hash{$digest} = $_; $package_names_by_hash{$digest} = $package_name; } } close $stdout; waitpid($pid, 0); } } find({ wanted => sub { return unless -f; return if -l; return if excludefile($_); my $digest = digest_file($_, 'SHA-1'); if (exists $file_names_by_hash{$digest}) { my $target_path = $file_names_by_hash{$digest}; my ($source_path) = $_ =~ m{debian/$package(/.*)}; my $target_relpath = File::Spec->abs2rel($target_path, dirname($source_path)); unlink($_) or error("Failed to delete $_: $!"); symlink($target_relpath, $_) or error("Failed to create symbolic link: $!"); my $dependency_name = $package_names_by_hash{$digest}; $package_dependencies{$package}{$dependency_name} = 1; } }, no_chdir => 1 }, $path); } sub write_substvars { my ($package) = @_; my $dependencies = $package_dependencies{$package}; while (my ($dependency, $versioned) = each %{$dependencies}) { if ($versioned) { my $stdout; my $pid = open2($stdout, undef, 'dpkg-query', '--show', '--showformat=${Version}', $dependency); waitpid($pid, 0); my $version = <$stdout>; $dependency = "$dependency (>= $version)"; } addsubstvar($package, 'mkdocs:Depends', $dependency); } } =head1 SEE ALSO L, L. This program is meant to be used together with debhelper. =head1 AUTHOR Dmitry Shachnev =cut