summaryrefslogtreecommitdiff
path: root/support/ctanify/ctanify
blob: e168ed6ede80edeee47897d2b36f7e18d2811b1e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
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.