summaryrefslogtreecommitdiff
path: root/support/ctanify/ctanify
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/ctanify/ctanify
Initial commit
Diffstat (limited to 'support/ctanify/ctanify')
-rwxr-xr-xsupport/ctanify/ctanify716
1 files changed, 716 insertions, 0 deletions
diff --git a/support/ctanify/ctanify b/support/ctanify/ctanify
new file mode 100755
index 0000000000..e168ed6ede
--- /dev/null
+++ b/support/ctanify/ctanify
@@ -0,0 +1,716 @@
+#! /usr/bin/env perl
+
+################################################
+# Prepare a LaTeX package for upload to CTAN #
+# By Scott Pakin <scott+ctify@pakin.org> #
+################################################
+
+use Cwd;
+use File::Basename;
+use File::Copy::Recursive qw(fcopy);
+use File::Find;
+use File::Path;
+use File::Spec;
+use File::stat;
+use File::Temp qw(tempdir);
+use Getopt::Long;
+use Pod::Usage;
+use warnings;
+use strict;
+
+# Define some global variables.
+our $VERSION = "1.9.1"; # ctanify version number
+my $progname = basename $0; # Name of this program
+my $pkgname; # Base name of the package to create
+my $miscify = 0; # 1=replace singletons with misc; 0=don't
+my $autoinclude = 1; # 1=automatically include files named in .ins
+my $skipdroppings = 1; # 1=skip "dropping" files (e.g., "README~")
+my $unixify = 1; # 1=make text files use Unix line endings
+my $maketds = 1; # 1=embed a .tds.zip file; 0=don't
+my @manifest; # List of files to include
+my %file2tds; # Map from specific filenames to TDS directories
+my @tdsdirlist; # Contents of the TDS tree
+my @pkgdirlist; # Contents of the package tree
+my @tdsonly; # Files to include only in the TDS tree
+my $tdsdir = ""; # Absolute directory of the TDS tree
+my $pkgdir = ""; # Absolute directory of the package directory
+my $zipname; # Name of the TDS zip file
+my $tarname; # Name of the package tar file
+my $tdsoutdir; # Directory name where to output TDS tree
+my $texmacros = "latex"; # TeX macro package
+
+# Associate file extensions with TDS directories. "%s" is replaced
+# with the package name.
+my %ext2tds =
+ ("README" => "doc/latex/%s",
+ "afm" => "fonts/afm/public/%s",
+ "bat" => "scripts/%s",
+ "bbx" => "tex/latex/%s",
+ "bib" => "bibtex/bib/%s",
+ "bst" => "bibtex/bst/%s",
+ "cbx" => "tex/latex/%s",
+ "cls" => "tex/latex/%s",
+ "dbx" => "tex/latex/%s",
+ "dtx" => "source/latex/%s",
+ "dvi" => "doc/latex/%s",
+ "fd" => "tex/latex/%s",
+ "ins" => "source/latex/%s",
+ "map" => "fonts/map/dvips/%s",
+ "md" => "doc/latex/%s",
+ "mf" => "fonts/source/public/%s",
+ "mp" => "metapost/%s",
+ "ofm" => "fonts/ofm/public/%s",
+ "ovf" => "fonts/ovf/public/%s",
+ "ovp" => "fonts/ovp/public/%s",
+ "pdf" => "doc/latex/%s",
+ "pfb" => "fonts/type1/public/%s",
+ "pfm" => "fonts/type1/public/%s",
+ "ps" => "doc/latex/%s",
+ "py" => "scripts/%s",
+ "sh" => "scripts/%s",
+ "sty" => "tex/latex/%s",
+ "tfm" => "fonts/tfm/public/%s",
+ "txt" => "doc/latex/%s",
+ "vf" => "fonts/vf/public/%s");
+
+# Specify a list of filename regexps to ignore.
+my @ignore_re = ('~$', '\bCVS\b', '\b\.svn\b');
+
+# Specify a list of file extensions that are considered "text" files.
+# This script will convert their line endings to Unix style (a single
+# linefeed character).
+my %text_ext =
+ map {($_ => 1)} qw(afm bib bst cls dtx fd ins ltx md mf mp sty tex txt);
+
+# Define a subroutine that returns the size in bytes of a file,
+# aborting on error.
+sub filesize ($)
+{
+ my $finfo = lstat($_[0]) || die "${progname}: Failed to stat $_ ($!)\n";
+ return $finfo->size;
+}
+
+# Define a subroutine that runs a command and aborts on error.
+sub run_command ($;$)
+{
+ my $command = $_[0];
+ my $ignore_errors = defined $_[1] ? $_[1] : 0;
+ my $retval = system $command;
+ if (!$ignore_errors && $retval>>8) {
+ die "${progname}: Command \"$command\" failed\n";
+ }
+}
+
+# Define a subroutine that Unix-ifies the line endings in text files.
+# This subroutine is called from File::Find so it does not accept
+# arguments via the @_ variable.
+my %reported_line_endings; # Files we converted and told the user about
+sub use_unix_line_endings
+{
+ # Determine if the file is a text file.
+ my $fname = $_;
+ return if !-f $fname;
+ my $fext = ($fname =~ m|([^./]+)$| && $1);
+ return if !defined $text_ext{$fext};
+
+ # Make it use Unix line endings. Note that we don't bother
+ # checking if we're already on a Unix (or Unix-like) system.
+ my $oldsize = filesize $fname;
+ open(TEXTFILE, "<$fname") || die "${progname}: Failed to open $File::Find::name ($!)\n";
+ my @entirefile = map {s/[\n\r]+$/\n/; $_} <TEXTFILE>;
+ close TEXTFILE;
+ open(BINARYFILE, ">$fname") || die "${progname}: Failed to open $File::Find::name ($!)\n";
+ binmode BINARYFILE;
+ print BINARYFILE @entirefile;
+ close BINARYFILE;
+ my $newsize = filesize $fname;
+ if ($newsize != $oldsize && !defined $reported_line_endings{$fname}) {
+ # Don't frighten the user when he sees a different file size
+ # in the tar file from the original in the filesystem
+ my $fullname = $File::Find::name;
+ $fullname =~ s,^$tdsdir/,,;
+ $fullname =~ s,^$pkgdir/,,;
+ warn "${progname}: Modified $fullname to use Unix line endings (use --no-unixify to prevent this)\n";
+ $reported_line_endings{$fname} = 1;
+ }
+}
+
+###########################################################################
+
+# Parse the command line.
+my $wanthelp = 0;
+my $wantversion = 0;
+GetOptions("p|pkgname=s" => \$pkgname,
+ "m|misc!" => \$miscify,
+ "k|skip!" => \$skipdroppings,
+ "u|unixify!" => \$unixify,
+ "t|tdsonly=s" => \@tdsonly,
+ "a|auto!" => \$autoinclude,
+ "d|tdsdir=s" => \$tdsoutdir,
+ "T|tex=s" => \$texmacros,
+ "s|tds!" => \$maketds,
+ "V|version" => \$wantversion,
+ "h|help" => \$wanthelp)
+ || pod2usage(-verbose => 0,
+ -exitval => 1);
+pod2usage(-verbose => 1,
+ -exitval => 0) if $wanthelp;
+if ($wantversion) {
+ print "ctanify version $VERSION\n";
+ exit 0;
+}
+pod2usage(-verbose => 0,
+ -exitval => 1) if !@ARGV;
+foreach my $fname (@ARGV) {
+ # Let the user specify "<filename>=<dirname>" to place files
+ # explicitly in the TDS tree.
+ if ($fname =~ /^(.*)=([^=]+)$/) {
+ foreach my $exp_fname (glob $1) {
+ $file2tds{$exp_fname} = $2;
+ push @manifest, $exp_fname;
+ }
+ }
+ else {
+ push @manifest, glob $fname;
+ }
+}
+@tdsonly = map {File::Spec->abs2rel($_)} map {glob} @tdsonly;
+
+# Replace "latex" with something else if --tex was specified.
+if ($texmacros ne "latex") {
+ foreach my $ext (keys %ext2tds) {
+ $ext2tds{$ext} =~ s,\blatex\b,$texmacros,g;
+ }
+}
+
+# Determine the package name if not explicitly specified.
+if (!defined $pkgname) {
+ my @mainfiles = (grep(/\.ins$/, @manifest), grep(/\.sty$/, @manifest));
+ if (!@mainfiles) {
+ die "${progname}: Please either list a .ins or .sty file or specify --pkgname\n";
+ }
+ $pkgname = basename $mainfiles[0];
+ $pkgname =~ s/\.[^.]+$//;
+}
+
+# Parse each .ins file to find more files to include.
+if ($autoinclude) {
+ foreach my $fname (grep /\.ins$/, @manifest) {
+ # Read the entire file.
+ local $/ = undef;
+ open(INSFILE, "<$fname") || die "${progname}: Failed to open $fname ($!)\n";
+ my $insfile = <INSFILE>;
+ close INSFILE;
+ $insfile =~ s/\%.*?\n//g;
+
+ # Add all source files (\from) to the manifest.
+ while ($insfile =~ /\\from\{([^\}]+)\}/g) {
+ my $fname_exp = $1;
+ $fname_exp =~ s/\\jobname\b/$pkgname/g;
+ push @manifest, $fname_exp;
+ }
+
+ # Add all generated files (\file) to the manifest but also to
+ # the list of TDS-only inclusions.
+ while ($insfile =~ /\\file\{([^\}]+)\}/g) {
+ my $fname_exp = $1;
+ $fname_exp =~ s/\\jobname\b/$pkgname/g;
+ push @manifest, $fname_exp;
+ push @tdsonly, $fname_exp;
+ }
+ }
+}
+
+# Skip "dropping" files and files beginning with ".".
+if ($skipdroppings) {
+ my @newmanifest;
+ CHECK_DROPPING:
+ foreach my $fname (@manifest) {
+ if (substr($fname, 0, 1) eq ".") {
+ warn "${progname}: Excluding $fname (use --no-skip to force inclusion)\n";
+ next CHECK_DROPPING;
+ }
+ foreach my $dropping_re (@ignore_re) {
+ if ($fname =~ $dropping_re) {
+ warn "${progname}: Excluding $fname (use --no-skip to force inclusion)\n";
+ next CHECK_DROPPING;
+ }
+ }
+ push @newmanifest, $fname;
+ }
+ @manifest = @newmanifest;
+}
+
+# Map each file to a TDS directory.
+foreach my $fname (@manifest) {
+ next if defined $file2tds{$fname};
+ my $fext = ($fname =~ m|([^./]+)$| && $1);
+ if ($fname =~ /\.tex$/ && $pkgname eq basename $fname, ".tex") {
+ # <package>.tex -- treat as documentation.
+ $file2tds{$fname} = sprintf $ext2tds{"README"}, $pkgname;
+ }
+ elsif (defined $ext2tds{$fext}) {
+ # Most files -- determine the directory based on the file extension.
+ $file2tds{$fname} = sprintf $ext2tds{$fext}, $pkgname;
+ }
+ elsif ($fext =~ /^(\d+)[a-z]*$/i) {
+ # Man page (e.g., "ls.1" or "File::Temp.3pm").
+ $file2tds{$fname} = "doc/man/man$1";
+ }
+ else {
+ # Read the file to determine if it's a Unix script (starts with "#!").
+ open(SCRIPT, "<", $fname) || die "${progname}: Failed to open $fname ($!)\n";
+ my $shebang;
+ read(SCRIPT, $shebang, 2);
+ close SCRIPT;
+ if ($shebang eq "#!") {
+ $file2tds{$fname} = "scripts/$pkgname";
+ }
+ else {
+ warn "${progname}: Not including $fname in the TDS tree (unknown extension)\n";
+ }
+ }
+}
+
+# Create a working directory and populate it with TDS files.
+my $workdir = tempdir("$progname-XXXXXX", TMPDIR => 1, CLEANUP => 1);
+$tdsdir = $tdsoutdir ? $tdsoutdir : "$workdir/texmf";
+mkdir $tdsdir || die "${progname}: Failed to create $tdsdir ($!)\n";
+foreach my $fname (@manifest) {
+ # Create a TDS subdirectory.
+ my $subdir = $file2tds{$fname};
+ next if !defined $subdir;
+ mkpath "$tdsdir/$subdir";
+
+ # Copy the specified file into the subdirectory.
+ fcopy($fname, "$tdsdir/$subdir/" . basename $fname) || die "${progname}: Failed to copy $fname ($!)\n";
+}
+if ($miscify) {
+ # Replace package directories containing a single file with "misc".
+ my %renamings;
+ find(sub {
+ return if $File::Find::name =~ m|\btexmf/fonts\b|;
+ if (-d) {
+ my @children = glob "$_/*";
+ if ($#children == 0 && -f $children[0] && $_ eq $pkgname) {
+ $renamings{$File::Find::name} = $File::Find::dir . "/misc";
+ }
+ }
+ }, $tdsdir);
+ while (my ($oldname, $newname) = each %renamings) {
+ rename $oldname, $newname;
+ }
+}
+if ($unixify) {
+ # Make all text files use Unix line endings.
+ find(\&use_unix_line_endings, $tdsdir);
+}
+
+# Complain if the doc directory contains PostScript or DVI files.
+my @bad_docs;
+find(sub {
+ return if !-f;
+ push @bad_docs, $_ if $File::Find::name =~ m,\bdoc/[^/]+/$pkgname/.*\.(ps|dvi)$,;
+}, $tdsdir);
+if (@bad_docs) {
+ my $bad_docs = join ", ", @bad_docs;
+ warn "${progname}: CTAN prefers having only PDF documentation (re: $bad_docs)\n";
+}
+
+# We have no more work to do if we're simply creating a TDS directory.
+exit 0 if $tdsoutdir;
+
+# Store a listing of the TDS directory.
+my $prevdir = getcwd();
+chdir $tdsdir || die "${progname}: Failed to switch to $tdsdir ($!)\n";
+find(sub {
+ return if !-f;
+ my $fsize = filesize $_;
+ push @tdsdirlist, [substr($File::Find::name, 2), $fsize];
+}, ".");
+
+# Archive and remove the TDS directory.
+$zipname = "$pkgname.tds.zip";
+my $zip_exclusions = $skipdroppings ? "-x __MACOSX -x .DS_Store" : "";
+run_command "zip -q -r -9 -y -m $workdir/$zipname . $zip_exclusions", 1;
+my $zipsize;
+if (-f "$workdir/$zipname") {
+ $zipsize = filesize "$workdir/$zipname";
+}
+else {
+ $zipname = "";
+}
+$zipname = "" if !$maketds;
+chdir $prevdir || die "${progname}: Failed to switch to $prevdir ($!)\n";
+rmdir $tdsdir || die "${progname}: Failed to remove $tdsdir ($!)\n";
+
+# Copy all files named in the file manifest to the package directory.
+$pkgdir = "$workdir/$pkgname";
+mkdir $pkgdir || die "${progname}: Failed to create $pkgdir ($!)\n";
+my %tdsonly = map {($_ => 1)} @tdsonly;
+foreach my $fname (@manifest) {
+ my $relname = File::Spec->abs2rel($fname);
+ next if defined $tdsonly{$relname};
+ my ($namepart, $pathpart, $suffixpart) = fileparse($relname);
+ mkpath "$pkgdir/$pathpart";
+ my $targetfile = "$pkgdir/$pathpart/$namepart$suffixpart";
+ fcopy($fname, $targetfile) || die "${progname}: Failed to copy $fname ($!)\n";
+}
+if ($unixify) {
+ # Make all text files use Unix line endings.
+ find(\&use_unix_line_endings, $pkgdir);
+}
+
+# Store a listing of the package directory.
+$prevdir = getcwd();
+chdir $pkgdir || die "${progname}: Failed to switch to $pkgdir ($!)\n";
+find(sub {
+ return if !-f;
+ my $fsize = filesize $_;
+ push @pkgdirlist, [substr($File::Find::name, 2), $fsize];
+}, ".");
+chdir $prevdir || die "${progname}: Failed to switch to $prevdir ($!)\n";
+
+# Tar up the package directory.
+$tarname = "$pkgname.tar.gz";
+my $tar_exclusions = $skipdroppings ? "--exclude=__MACOSX --exclude=.DS_Store" : "";
+run_command "tar -czf $tarname -C $workdir $tar_exclusions $pkgname $zipname";
+
+# Output a listing of what we tarred up.
+printf "\n%8d %s\n\n", filesize($tarname), $tarname;
+foreach my $fname_size (sort {$a->[0] cmp $b->[0]} @pkgdirlist) {
+ my ($fname, $fsize) = @$fname_size;
+ printf " %8d %s/%s\n", $fsize, $pkgname, $fname;
+}
+if ($zipname ne "") {
+ printf " %8d %s\n\n", $zipsize, $zipname;
+ foreach my $fname_size (sort {$a->[0] cmp $b->[0]} @tdsdirlist) {
+ my ($fname, $fsize) = @$fname_size;
+ printf " %8d %s\n", $fsize, $fname;
+ }
+}
+print "\n";
+
+###########################################################################
+
+__END__
+
+=head1 NAME
+
+ctanify - Prepare a package for upload to CTAN
+
+
+=head1 SYNOPSIS
+
+ctanify
+[B<--pkgname>=I<string>]
+[B<-->[B<no>]B<auto>]
+[B<--tdsonly>=I<filespec> ...]
+[B<-->[B<no>]B<unixify>]
+[B<-->[B<no>]B<skip>]
+[B<--tdsdir>=I<dirname> ...]
+[B<--tex>=I<macro_pkg>]
+[B<-->[B<no>]B<miscify>]
+[B<-->[B<no>]B<tds>]
+I<filespec>[=I<dirname>] ...
+
+ctanify
+[B<--help>]
+
+ctanify
+[B<--version>]
+
+
+=head1 DESCRIPTION
+
+B<ctanify> is intended for developers who have a LaTeX package that
+they want to distribute via the Comprehensive TeX Archive Network
+(CTAN). Given a list of filenames, B<ctanify> creates a tarball (a
+F<.tar.gz> file) with the files laid out in CTAN's preferred
+structure. The tarball additionally contains a ZIP (F<.zip>) file
+with copies of all files laid out in the standard TeX Directory
+Structure (TDS), which facilitates inclusion of the package in the TeX
+Live distribution.
+
+
+=head1 OPTIONS
+
+B<ctanify> accepts the following command-line options:
+
+=over 5
+
+=item B<-h>, B<--help>
+
+Output basic usage information and exit.
+
+=item B<-V>, B<--version>
+
+Output B<ctanify>'s version number and exit.
+
+=item B<-p> I<string>, B<--pkgname>=I<string>
+
+Specify explicitly a package name. Normally, B<ctanify> uses the base
+name of the first F<.ins> or F<.sty> file listed as the package name.
+The package name forms the base name of the tarball that B<ctanify>
+produces.
+
+=item B<--noauto>
+
+Do not automatically add files to the tarball. Normally, B<ctanify>
+automatically includes all files mentioned in a F<.ins> file.
+
+=item B<-t> I<filespec>, B<--tdsonly>=I<filespec>
+
+Specify a subset of the files named on the command line to include
+only in the TDS ZIP file, not in the CTAN package directory.
+Wildcards are allowed (quoted if necessary), and B<--tdsonly> can be
+used multiple times on the same command line.
+
+=back
+
+At least one filename must be specified on the command line.
+B<ctanify> automatically places files in the TDS tree based on their
+extension, but this can be overridden by specifying explicitly a
+target TDS directory using the form I<filespec>=I<dirname>. Wildcards
+are allowed for the filespec (quoted if necessary).
+
+
+=head1 ADDITIONAL OPTIONS
+
+The following options are unlikely to be necessary in ordinary usage.
+They are provided for special circumstances that may arise.
+
+=over 5
+
+=item B<-d> I<dirname>, B<--tdsdir>=I<dirname>
+
+Instead of creating a tarball for CTAN, merely create the package TDS
+tree rooted in directory I<dirname>.
+
+=item B<-T> I<macro_pkg>, B<--tex>=I<macro_pkg>
+
+Assert that the files being packaged for CTAN target a TeX macro
+package other than LaTeX. Some common examples of I<macro_pkg> are
+C<generic>, C<plain>, and C<context>.
+
+=item B<-nou>, B<--no-unixify>
+
+Store text files unmodified instead of converting their end-of-line
+character to Unix format (a single linefeed character with no
+carriage-return character), even though CTAN prefers receiving all
+files with Unix-format end-of-line characters.
+
+=item B<-nok>, B<--no-skip>
+
+Force B<ctanify> to include files such as Unix hidden files, Emacs
+backup files, and version-control metadata files, all of which CTAN
+dislikes receiving.
+
+=item B<-m>, B<--miscify>
+
+Rename directories containing a single file to C<misc>. (For example,
+rename C<tex/latex/mypackage/mypackage.sty> to
+C<tex/latex/misc/mypackage.sty>.) This was common practice in the
+past but is now strongly discouraged.
+
+=item B<-nos>, B<--no-tds>
+
+Do not embed a .tds.zip file in the generated tarball.
+
+=back
+
+
+=head1 DIAGNOSTICS
+
+=over 5
+
+=item C<Failed to copy I<filename> (No such file or directory)>
+
+This message is typically caused by a F<.ins> file that generates
+I<filename> but that has not already been run through F<tex> or
+F<latex> to actually produce I<filename>. B<ctanify> does not
+automatically run F<tex> or F<latex>; this needs to be done manually
+by the user. See L</CAVEATS> for more information.
+
+=item C<Modified I<filename> to use Unix line endings (use --no-unixify to prevent this)>
+
+For consistency, CTAN stores all text files with Unix-style line
+endings (a single linefeed character with no carriage-return
+character). To help in this effort, B<ctanify> automatically replaces
+non-Unix-style line endings. The preceding merely message notifies
+the user that he should not be alarmed to see a different size for
+I<filename> in the tarball versus the original I<filename> on disk
+(which B<ctanify> never modifies). If there's a good reason to
+preserve the original line endings (and there rarely is), the
+B<--no-unixify> option can be used to prevent B<ctanify> from altering
+any files when storing them in the tarball.
+
+=item C<Excluding I<filename> (use --no-skip to force inclusion)>
+
+B<ctanify> normally ignores files--even when specified explicitly on
+the command line--that CTAN prefers not receiving. These include
+files whose names start with "F<.>" (Unix hidden files), end in "F<~>"
+(Emacs automatic backups), or that come from a F<CVS> or F<.svn>
+directory (version-control metadata files). If there's a good reason
+to submit such files to CTAN (and there rarely is), the B<--no-skip>
+option can be used to prevent B<ctanify> from ignoring them.
+
+=item C<CTAN prefers having only PDF documentation (re: I<filename>)>
+
+Because of the popularity of the PDF format, CTAN wants to have as
+much documentation as possible distributed in PDF. The preceding
+message asks the user to replace any PostScript or DVI documentation
+with PDF if possible. (B<ctanify> will still include PostScript and
+DVI documentation in the tarball; the preceding message is merely a
+polite request.)
+
+=item C<Not including I<filename> in the TDS tree (unknown extension)>
+
+B<ctanify> places files in the TDS tree based on a table of file
+extensions. For example, all F<.sty> files are placed in
+F<tex/latex/I<package-name>>. If B<ctanify> does not know where to
+put a file it does not put it anywhere. See the last paragraph of
+L</OPTIONS> for an explanation of how to specify explicitly a file's
+target location in the TDS tree. For common file extensions that
+happen to be absent from B<ctanify>'s table, consider also notifying
+B<ctanify>'s author at the address shown below under L</AUTHOR>.
+
+=back
+
+
+=head1 EXAMPLES
+
+=head2 The Common Case
+
+Normally, all that's needed is to tell B<ctanify> the name of the
+F<.ins> file (or F<.sty> if the package does not use DocStrip) and the
+prebuilt documentation, if any:
+
+ $ ctanify mypackage.ins mypackage.pdf README
+
+ 490347 mypackage.tar.gz
+
+ 1771 mypackage/README
+ 15453 mypackage/mypackage.dtx
+ 1957 mypackage/mypackage.ins
+ 277683 mypackage/mypackage.pdf
+ 246935 mypackage.tds.zip
+
+ 1771 doc/latex/mypackage/README
+ 277683 doc/latex/mypackage/mypackage.pdf
+ 15453 source/latex/mypackage/mypackage.dtx
+ 1957 source/latex/mypackage/mypackage.ins
+ 1725 tex/latex/mypackage/mypackage.sty
+
+B<ctanify> outputs the size in bytes of the resulting tarball, each
+file within it, and each file within the contained ZIP file. In the
+preceding example, notice how B<ctanify> automatically performed all
+of the following operations:
+
+=over 5
+
+=item *
+
+including F<mypackage.dtx> (found by parsing F<mypackage.ins>) in both
+the F<mypackage> directory and the ZIP file,
+
+=item *
+
+including F<mypackage.sty> (found by parsing F<mypackage.ins>) in the
+ZIP file but, because it's a generated file, not in the F<mypackage>
+directory, and
+
+=item *
+
+placing all files into appropriate TDS directories (documentation,
+source, main package) within the ZIP file.
+
+=back
+
+Consider what it would take to manually produce an equivalent
+F<mypackage.tar.gz> file. B<ctanify> is definitely a simpler, quicker
+alternative.
+
+
+=head2 Advanced Usage
+
+B<ctanify> assumes that PostScript files are documentation and
+therefore stores them under F<doc/latex/I<package-name>/> in the TDS
+tree within the ZIP File. Suppose, however, that a LaTeX package uses
+a set of PostScript files to control B<dvips>'s output. In this case,
+B<ctanify> must be told to include those PostScript files in the
+package directory, not the documentation directory.
+
+ $ ctanify mypackage.ins "mypackage*.ps=tex/latex/mypackage"
+
+
+=head1 FILES
+
+=over 5
+
+=item F<perl>
+
+B<ctanify> is written in Perl and needs a Perl installation to run.
+
+=item F<tar>, F<gzip>
+
+B<ctanify> requires the GNU F<tar> and F<gzip> programs to create a
+compressed tarball (F<.tar.gz>).
+
+=item F<zip>
+
+B<ctanify> uses a F<zip> program to archive the TDS tree within the
+main tarball.
+
+=back
+
+
+=head1 CAVEATS
+
+B<ctanify> does not invoke F<tex> or F<latex> on its own, S<e.g., to>
+process a F<.ins> file. The reason is that B<ctanify> does not know
+in the general case how to produce all of a package's generated files.
+It was deemed better to do nothing than to risk overwriting existing
+F<.sty> (or other) files or to include outdated generated files in the
+tarball. In short, before running B<ctanify> you should manually
+process any F<.ins> files and otherwise generate any files that should
+be sent to CTAN.
+
+B<ctanify> has been tested only on Linux. It may work on S<OS X>.
+I've been told that it works on Windows when run using Cygwin.
+Volunteers willing to help port B<ctanify> to other platforms are
+extremely welcome.
+
+
+=head1 SEE ALSO
+
+tar(1),
+zip(1),
+latex(1),
+Guidelines for uploading TDS-Packaged materials to CTAN
+(L<http://www.ctan.org/TDS-guidelines>),
+A Directory Structure for TeX Files (L<http://tug.org/tds/>),
+
+
+=head1 AUTHOR
+
+Scott Pakin, I<scott+ctify@pakin.org>
+
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2017 Scott Pakin
+
+This work may be distributed and/or modified under the conditions of
+the LaTeX Project Public License, either S<version 1.3c> of this
+license or (at your option) any later version. The latest version of
+this license is in
+
+=over 4
+
+L<http://www.latex-project.org/lppl.txt>
+
+=back
+
+and S<version 1.3c> or later is part of all distributions of LaTeX
+version 2008/05/04 or later.