I tried my hand at implementing Frank Mittelbach's proposal of removing packages one at a time, testing for each one whether removing it changes the output.
This is done automatically by a script written purely in LaTeX (which requires the --shell-escape). The resulting code is very slow: it has to run TeX on the file many times (twice the number of packages), and TeX is not the fastest tool to parse the resulting log file (that step could be made faster, actually).
To use it, store the following code in a file, say packagecheck.tex, and call pdflatex --shell-escape packagecheck <engine> <filename> where <engine> <filename> is the command you would usually type.
%http://tex.stackexchange.com/questions/78073/detecting-unused-packages-in-latex-document?lq=1
\ifnum\pdfshellescape=1\else
\PackageError{packagecheck}
{Misused\MessageBreak
pdflatex --shell-escape packagecheck\MessageBreak
<engine> <file-name>}
{}
\csname @@end\expandafter\endcsname
\csname bye\expandafter\endcsname
\fi
\RequirePackage{expl3, l3regex}
\ExplSyntaxOn
% \begin{macro}{\log_map_inline:nn, \log_map_inline:Vn}
% \begin{syntax}
% \cs{log_map_inline:nn} \Arg{file name} \Arg{code}
% \end{syntax}
% Goes through the file \meta{file name}\texttt{.log} and performs the
% \meta{code} for each logical line of the log file (assuming that any
% line with $79$ characters in the log file is to be concatenated with
% the following one). The \meta{code} receives the line as |#1|, as a
% string. For instance the following shows each line in |texput.log|.
% \begin{verbatim}
% \log_map_inline:nn { texput } { \tl_show:n {#1} }
% \end{verbatim}
% \begin{macrocode}
\int_const:Nn \c_log_line_count_int { 79 }
\int_new:N \l_log_line_int
\int_new:N \l_log_max_line_int
\ior_new:N \g_log_ior
\str_new:N \l_log_line_str
\str_new:N \l_log_tmp_str
\cs_new_protected:Npn \__log_tmp:n #1 { }
\cs_new_protected:Npn \log_map_inline:nn #1#2 % not nestable
{
%<*trace>
\int_zero:N \l_log_line_int
\ior_open:Nn \g_log_ior { #1 . log }
\ior_str_map_inline:Nn \g_log_ior { \int_incr:N \l_log_line_int }
\ior_close:N \g_log_ior
\msg_term:n
{
Opening~file~#1.log~( \int_use:N \l_log_line_int \ lines).~
One~dot~per~100~lines.
}
\int_zero:N \l_log_line_int
%</trace>
\cs_set_protected:Npn \__log_tmp:n ##1 {#2}
\ior_open:Nn \g_log_ior { #1 . log }
\ior_str_map_inline:Nn \g_log_ior
{
%<*trace>
\int_incr:N \l_log_line_int
\if_int_compare:w \l_log_line_int > 100 \scan_stop:
\tex_message:D { . }
\int_zero:N \l_log_line_int
\fi:
%</trace>
\str_set:Nn \l_log_tmp_str {##1}
\tl_set_eq:NN \l_log_line_str \l_log_tmp_str
\int_until_do:nNnn
{ \str_count:N \l_log_tmp_str } < \c_log_line_count_int
{
\ior_get_str:NN \g_log_ior \l_log_tmp_str
\str_put_right:Nx \l_log_line_str \l_log_tmp_str
}
\exp_args:No \__log_tmp:n \l_log_line_str
}
\ior_close:N \g_log_ior
}
\cs_generate_variant:Nn \log_map_inline:nn { V }
% \end{macrocode}
% \end{macro}
%
% \begin{macro}{\log_get_packages:nN, \log_get_packages:VN}
% \begin{macrocode}
\seq_new:N \l_log_packages_seq
\seq_new:N \l_log_tmp_seq
\regex_const:Nn \c_package_regex { \w* \. sty }
\cs_new_protected:Npn \log_get_packages:nN #1#2
{
\seq_clear:N \l_log_packages_seq
\log_map_inline:nn {#1}
{
\regex_extract_all:NnN \c_package_regex {##1} \l_log_tmp_seq
\seq_concat:NNN \l_log_packages_seq
\l_log_packages_seq \l_log_tmp_seq
}
\seq_remove_duplicates:N \l_log_packages_seq
\tl_set:Nx #2
{ \seq_map_function:NN \l_log_packages_seq \prg_do_nothing: }
}
\cs_generate_variant:Nn \log_get_packages:nN { V }
% \end{macrocode}
% \end{macro}
%
% \begin{macro}{\log_get_pages:nN}
% \begin{macrocode}
\tl_new:N \l_log_pages_tl
\str_new:N \l_log_page_str
\bool_new:N \l_log_in_page_bool
\cs_new_protected:Npn \log_get_pages:nN #1#2
{
\tl_clear:N \l_log_pages_tl
\tl_clear:N \l_log_page_str
\bool_set_false:N \l_log_in_page_bool
\log_map_inline:nn {#1} % could be optimized away
{
\bool_if:NTF \l_log_in_page_bool
{
\tl_if_empty:nTF {##1}
{
\bool_set_false:N \l_log_in_page_bool
\tl_put_right:Nx \l_log_pages_tl { { \l_log_page_str } }
}
{ \str_put_right:Nx \l_log_page_str { ##1 \iow_newline: } }
}
{
\str_if_eq_x:nnT
{ Completed } { \__log_get_pages_test:ww ##1 ~ \q_stop }
{
\str_if_eq_x:nnT
{ Completed~box~being~shipped~out~ }
{ \str_substr:nnn {##1} { 1 } { 32 } }
{
\bool_set_true:N \l_log_in_page_bool
\str_put_right:Nx \l_log_page_str { ##1 \iow_newline: }
}
}
}
}
\tl_set_eq:NN #2 \l_log_pages_tl
}
\cs_new:Npn \__log_get_pages_test:ww #1 ~ #2 \q_stop {#1}
\cs_generate_variant:Nn \log_get_pages:nN { V }
% \end{macrocode}
% \end{macro}
%
% \begin{macro}[aux]{\__run:n}
% Compile the file once, halting if there is an error, and in
% batchmode to suppress the output. First output a line to separate
% the output of the internal \TeX{} run from the main run.
% \begin{macrocode}
\cs_new_protected:Npn \__run:n #1
{
\iow_term:x { }
\tex_immediate:D \tex_write:D 18
{
\l_run_engine_str \c_space_tl
-halt-on-error \c_space_tl
-interaction=batchmode \c_space_tl
'\tl_to_str:n { #1 \input }' \c_space_tl
\l_run_file_str
}
}
% \end{macrocode}
% \end{macro}
%
\str_new:N \l_run_engine_str
\str_new:N \l_run_file_str
\tl_new:N \l_run_packages_tl
\tl_new:N \l_run_normal_pages_tl
\tl_new:N \l_run_pages_tl
\seq_new:N \l_run_used_packages_seq
\seq_new:N \l_run_unused_packages_seq
\int_new:N \l_run_package_int
\int_new:N \l_run_package_max_int
\cs_new_protected:Npn \__run_get_pages:nN #1#2
{
\__run:n {#1}
\__run:n { #1 \loggingoutput \batchmode }
\log_get_pages:VN \l_run_file_str #2
}
\cs_new_protected:Npn \run:ww #1~#2~
{
\str_set:Nx \l_run_engine_str {#1}
\str_set:Nx \l_run_file_str {#2}
\regex_replace_once:nnN { \. tex \Z } { } \l_run_file_str
\__run:n { }
\log_get_packages:VN \l_run_file_str \l_run_packages_tl
\__run_get_pages:nN { } \l_run_normal_pages_tl
%<*trace>
\int_set:Nn \l_run_package_max_int { \tl_count:N \l_run_packages_tl }
\int_zero:N \l_run_package_int
%</trace>
\tl_map_inline:Nn \l_run_packages_tl
{
%<*trace>
\int_incr:N \l_run_package_int
\msg_term:n
{
Package~\int_use:N \l_run_package_int \
of~\int_use:N \l_run_package_max_int
}
%</trace>
\__run_get_pages:nN
{ \expandafter \let \csname ver @ ##1 \endcsname \empty }
\l_run_pages_tl
\tl_if_eq:NNTF \l_run_pages_tl \l_run_normal_pages_tl
{ \seq_put_right:Nn \l_run_unused_packages_seq {##1} }
{ \seq_put_right:Nn \l_run_used_packages_seq {##1} }
}
\seq_if_empty:NF \l_run_used_packages_seq
{
\msg_info:nnxx { run } { used-packages }
{ \seq_count:N \l_run_used_packages_seq }
{
\seq_use:Nnnn \l_run_used_packages_seq
{ ~ and ~ } { , ~ } { , ~ and ~ }
}
}
\seq_if_empty:NF \l_run_unused_packages_seq
{
\msg_warning:nnxx { run } { unused-packages }
{ \seq_count:N \l_run_unused_packages_seq }
{
\seq_use:Nnnn \l_run_unused_packages_seq
{ ~ and ~ } { , ~ } { , ~ and ~ }
}
}
\msg_term:n
{
\fp_to_tl:n { round(\pdfelapsedtime/65536,1) }
\ seconds
}
\tex_end:D
}
\msg_new:nnn { run } { used-packages }
{
\int_compare:nTF { #1 = 1 }
{ The~package~#2~is~useful~in~this~document }
{ The~#1~packages~#2~are~useful~in~this~document. }
}
\msg_new:nnn { run } { unused-packages }
{
Removing~
\int_compare:nTF { #1 = 1 } { the~package~ } { any~of~the~packages~ }
#2~changes~nothing~in~the~resulting~output.
}
\use:n
{
\ExplSyntaxOff
\tex_endinput:D
\exp_after:wN \run:ww
}%
As I said, this is very slow, for instance, running pdflatex --shell-escape packagecheck pdflatex test.tex on the file test.tex
\documentclass{article}
\usepackage{lipsum}
\usepackage{amsgen}
\begin{document}
\lipsum[1-10]
\end{document}
takes 5 minutes on my laptop before telling me that amsgen is not used in the document, but lipsum is.
How it works: the main tool is to call \immediate\write18{<engine> -halt-on-error -interaction=batchmode <TeX code> <filename>} where <engine> is, for instance, pdflatex, -halt-on-error ensures that TeX won't try to continue beyond the first error (there will be errors when removing some packages), -interaction=batchmode minimizes the output of the "internal" runs, <TeX code> is some code that varies with the different runs, and <filename> is, well, the name of the file to compile.
First run with no extra code. This produces a log file in which we look for everything that matches \w*.sty (word characters followed by sty). Perhaps I should search for [\w\-]*.sty instead, or something else.
Then run twice to produce the correct output, first without and then with \loggingoutput\batchmode, which ensures that (1) the output pages are detailed in the log file (2) nothing more is written to the terminal.
Then for each package, run twice, first without and then with \loggingoutput\batchmode, disabling the loading of that particular package by defining \ver@<package name>.sty prior to each run, to make LaTeX believe that the package is already loaded. Compare the pages found in the log file to the normal pages found in the previous step to find out if the package is useful.
Finally, show the list of all useless packages, and the time elapsed.
microtype. – doncherry Oct 17 '12 at 18:55fontenc,inputencforpdflatex;fontspecfor XeLaTeX and LuaLaTeX) and possiblybabelorpolyglossia; load other ones only when you need them. Exceptions might bymicrotypearrayandhyperref. – egreg Oct 17 '12 at 20:28