1#!/usr/bin/perl
2####################################################################################################################################
3# doc.pl - PgBackRest Doc Builder
4####################################################################################################################################
5
6####################################################################################################################################
7# Perl includes
8####################################################################################################################################
9use strict;
10use warnings FATAL => qw(all);
11use Carp qw(confess);
12use English '-no_match_vars';
13
14$SIG{__DIE__} = sub { Carp::confess @_ };
15
16use Cwd qw(abs_path);
17use File::Basename qw(dirname);
18use Getopt::Long qw(GetOptions);
19use Pod::Usage qw(pod2usage);
20use Storable;
21
22use lib dirname(abs_path($0)) . '/lib';
23use lib dirname(dirname(abs_path($0))) . '/lib';
24use lib dirname(dirname(abs_path($0))) . '/build/lib';
25use lib dirname(dirname(abs_path($0))) . '/test/lib';
26
27use pgBackRestTest::Common::ExecuteTest;
28use pgBackRestTest::Common::Storage;
29use pgBackRestTest::Common::StoragePosix;
30
31use pgBackRestDoc::Common::Doc;
32use pgBackRestDoc::Common::DocConfig;
33use pgBackRestDoc::Common::DocManifest;
34use pgBackRestDoc::Common::DocRender;
35use pgBackRestDoc::Common::Exception;
36use pgBackRestDoc::Common::Log;
37use pgBackRestDoc::Common::String;
38use pgBackRestDoc::Html::DocHtmlSite;
39use pgBackRestDoc::Latex::DocLatex;
40use pgBackRestDoc::Markdown::DocMarkdown;
41use pgBackRestDoc::ProjectInfo;
42
43####################################################################################################################################
44# Usage
45####################################################################################################################################
46
47=head1 NAME
48
49doc.pl - Generate pgBackRest documentation
50
51=head1 SYNOPSIS
52
53doc.pl [options]
54
55 General Options:
56   --help           Display usage and exit
57   --version        Display pgBackRest version
58   --quiet          Sets log level to ERROR
59   --log-level      Log level for execution (e.g. ERROR, WARN, INFO, DEBUG)
60   --deploy         Write exe.cache into resource for persistence
61   --no-exe         Should commands be executed when building help? (for testing only)
62   --no-cache       Don't use execution cache
63   --cache-only     Only use the execution cache - don't attempt to generate it
64   --pre            Pre-build containers for execute elements marked pre
65   --var            Override defined variable
66   --key-var        Override defined variable and use in cache key
67   --doc-path       Document path to render (manifest.xml should be located here)
68   --out            Output types (html, pdf, markdown)
69   --out-preserve   Don't clean output directory
70   --require        Require only certain sections of the document (to speed testing)
71   --include        Include source in generation (links will reference website)
72   --exclude        Exclude source from generation (links will reference website)
73
74 Variable Options:
75   --dev            Set 'dev' variable to 'y'
76   --debug          Set 'debug' variable to 'y'
77=cut
78
79####################################################################################################################################
80# Load command line parameters and config (see usage above for details)
81####################################################################################################################################
82my $bHelp = false;
83my $bVersion = false;
84my $bQuiet = false;
85my $strLogLevel = 'info';
86my $bNoExe = false;
87my $bNoCache = false;
88my $bCacheOnly = false;
89my $rhVariableOverride = {};
90my $rhKeyVariableOverride = {};
91my $strDocPath;
92my @stryOutput;
93my $bOutPreserve = false;
94my @stryRequire;
95my @stryInclude;
96my @stryExclude;
97my $bDeploy = false;
98my $bDev = false;
99my $bDebug = false;
100my $bPre = false;
101
102GetOptions ('help' => \$bHelp,
103            'version' => \$bVersion,
104            'quiet' => \$bQuiet,
105            'log-level=s' => \$strLogLevel,
106            'out=s@' => \@stryOutput,
107            'out-preserve' => \$bOutPreserve,
108            'require=s@' => \@stryRequire,
109            'include=s@' => \@stryInclude,
110            'exclude=s@' => \@stryExclude,
111            'no-exe', \$bNoExe,
112            'deploy', \$bDeploy,
113            'no-cache', \$bNoCache,
114            'dev', \$bDev,
115            'debug', \$bDebug,
116            'pre', \$bPre,
117            'cache-only', \$bCacheOnly,
118            'key-var=s%', $rhKeyVariableOverride,
119            'var=s%', $rhVariableOverride,
120            'doc-path=s', \$strDocPath)
121    or pod2usage(2);
122
123####################################################################################################################################
124# Run in eval block to catch errors
125####################################################################################################################################
126eval
127{
128    # Display version and exit if requested
129    if ($bHelp || $bVersion)
130    {
131        print PROJECT_NAME . ' ' . PROJECT_VERSION . " Documentation Builder\n";
132
133        if ($bHelp)
134        {
135            print "\n";
136            pod2usage();
137        }
138
139        exit 0;
140    }
141
142    # Disable cache when no exe
143    if ($bNoExe)
144    {
145        $bNoCache = true;
146    }
147
148    # Make sure options are set correctly for deploy
149    if ($bDeploy)
150    {
151        my $strError = 'cannot be specified for deploy';
152
153        !$bNoExe
154            or confess "--no-exe ${strError}";
155
156        !@stryRequire
157            or confess "--require ${strError}";
158    }
159
160    # one --include must be specified when --required is
161    if (@stryRequire && @stryInclude != 1)
162    {
163        confess "one --include is required when --require is specified";
164    }
165
166    # Set console log level
167    if ($bQuiet)
168    {
169        $strLogLevel = 'error';
170    }
171
172    # If --dev passed then set the dev var to 'y'
173    if ($bDev)
174    {
175        $rhVariableOverride->{'dev'} = 'y';
176    }
177
178    # If --debug passed then set the debug var to 'y'
179    if ($bDebug)
180    {
181        $rhVariableOverride->{'debug'} = 'y';
182    }
183
184    # Doesn't make sense to pass include and exclude
185    if (@stryInclude > 0 && @stryExclude > 0)
186    {
187        confess "cannot specify both --include and --exclude";
188    }
189
190    logLevelSet(undef, uc($strLogLevel), OFF);
191
192    # Get the base path
193    my $strBasePath = abs_path(dirname($0));
194
195    my $oStorageDoc = new pgBackRestTest::Common::Storage(
196        $strBasePath, new pgBackRestTest::Common::StoragePosix({bFileSync => false, bPathSync => false}));
197
198    if (!defined($strDocPath))
199    {
200        $strDocPath = $strBasePath;
201    }
202
203    my $strOutputPath = "${strDocPath}/output";
204
205    # Create the out path if it does not exist
206    if (!-e $strOutputPath)
207    {
208        mkdir($strOutputPath)
209            or confess &log(ERROR, "unable to create path ${strOutputPath}");
210    }
211
212    # Merge key variables into the variable list and ensure there are no duplicates
213    foreach my $strKey (sort(keys(%{$rhKeyVariableOverride})))
214    {
215        if (defined($rhVariableOverride->{$strKey}))
216        {
217            confess &log(ERROR, "'${strKey}' cannot be passed as --var and --key-var");
218        }
219
220        $rhVariableOverride->{$strKey} = $rhKeyVariableOverride->{$strKey};
221    }
222
223    # Load the manifest
224    my $oManifest = new pgBackRestDoc::Common::DocManifest(
225        $oStorageDoc, \@stryRequire, \@stryInclude, \@stryExclude, $rhKeyVariableOverride, $rhVariableOverride,
226        $strDocPath, $bDeploy, $bCacheOnly, $bPre);
227
228    if (!$bNoCache)
229    {
230        $oManifest->cacheRead();
231    }
232
233    # If no outputs were given
234    if (@stryOutput == 0)
235    {
236        @stryOutput = $oManifest->renderList();
237
238        if ($oManifest->isBackRest())
239        {
240            push(@stryOutput, 'man');
241        }
242    }
243
244    # Build host containers
245    if (!$bCacheOnly && !$bNoExe)
246    {
247        foreach my $strSource ($oManifest->sourceList())
248        {
249            if ((@stryInclude == 0 || grep(/$strSource/, @stryInclude)) && !grep(/$strSource/, @stryExclude))
250            {
251                &log(INFO, "source $strSource");
252
253                foreach my $oHostDefine ($oManifest->sourceGet($strSource)->{doc}->nodeList('host-define', false))
254                {
255                    if ($oManifest->evaluateIf($oHostDefine))
256                    {
257                        my $strImage = $oManifest->variableReplace($oHostDefine->paramGet('image'));
258                        my $strFrom = $oManifest->variableReplace($oHostDefine->paramGet('from'));
259                        my $strDockerfile = "${strOutputPath}/doc-host.dockerfile";
260
261                        &log(INFO, "Build vm '${strImage}' from '${strFrom}'");
262
263                        $oStorageDoc->put(
264                            $strDockerfile,
265                            "FROM ${strFrom}\n\n" . trim($oManifest->variableReplace($oHostDefine->valueGet())) . "\n");
266                        executeTest("docker build -f ${strDockerfile} -t ${strImage} ${strBasePath}");
267                    }
268                }
269            }
270        }
271    }
272
273    # Render output
274    for my $strOutput (@stryOutput)
275    {
276        if (!($strOutput eq 'man' && $oManifest->isBackRest()))
277        {
278            $oManifest->renderGet($strOutput);
279        }
280
281        &log(INFO, "render ${strOutput} output");
282
283        # Clean contents of out directory
284        if (!$bOutPreserve)
285        {
286            my $strOutputPath = $strOutput eq 'pdf' ? "${strOutputPath}/latex" : "${strOutputPath}/$strOutput";
287
288            # Clean the current out path if it exists
289            if (-e $strOutputPath)
290            {
291                executeTest("rm -rf ${strOutputPath}/*");
292            }
293            # Else create the html path
294            else
295            {
296                mkdir($strOutputPath)
297                    or confess &log(ERROR, "unable to create path ${strOutputPath}");
298            }
299        }
300
301        if ($strOutput eq 'markdown')
302        {
303            my $oMarkdown =
304                new pgBackRestDoc::Markdown::DocMarkdown
305                (
306                    $oManifest,
307                    "${strBasePath}/xml",
308                    "${strOutputPath}/markdown",
309                    !$bNoExe
310                );
311
312            $oMarkdown->process();
313        }
314        elsif ($strOutput eq 'man' && $oManifest->isBackRest())
315        {
316            # Generate the command-line help
317            my $oRender = new pgBackRestDoc::Common::DocRender('text', $oManifest, !$bNoExe);
318            my $oDocConfig =
319                new pgBackRestDoc::Common::DocConfig(
320                    new pgBackRestDoc::Common::Doc("${strBasePath}/xml/reference.xml"), $oRender);
321
322            $oStorageDoc->pathCreate(
323                "${strBasePath}/output/man", {strMode => '0770', bIgnoreExists => true, bCreateParent => true});
324            $oStorageDoc->put("${strBasePath}/output/man/" . lc(PROJECT_NAME) . '.1.txt', $oDocConfig->manGet($oManifest));
325        }
326        elsif ($strOutput eq 'html')
327        {
328            my $oHtmlSite =
329                new pgBackRestDoc::Html::DocHtmlSite
330                (
331                    $oManifest,
332                    "${strBasePath}/xml",
333                    "${strOutputPath}/html",
334                    "${strBasePath}/resource/html/default.css",
335                    defined($oManifest->variableGet('project-favicon')) ?
336                        "${strBasePath}/resource/html/" . $oManifest->variableGet('project-favicon') : undef,
337                    defined($oManifest->variableGet('project-logo')) ?
338                        "${strBasePath}/resource/" . $oManifest->variableGet('project-logo') : undef,
339                    !$bNoExe
340                );
341
342            $oHtmlSite->process();
343        }
344        elsif ($strOutput eq 'pdf')
345        {
346            my $oLatex =
347                new pgBackRestDoc::Latex::DocLatex
348                (
349                    $oManifest,
350                    "${strBasePath}/xml",
351                    "${strOutputPath}/latex",
352                    "${strBasePath}/resource/latex/preamble.tex",
353                    !$bNoExe
354                );
355
356            $oLatex->process();
357        }
358    }
359
360    # Cache the manifest (mostly useful for testing rendering changes in the code)
361    if (!$bNoCache && !$bCacheOnly)
362    {
363        $oManifest->cacheWrite();
364    }
365
366    # Exit with success
367    exit 0;
368}
369
370####################################################################################################################################
371# Check for errors
372####################################################################################################################################
373or do
374{
375    # If a backrest exception then return the code
376    exit $EVAL_ERROR->code() if (isException(\$EVAL_ERROR));
377
378    # Else output the unhandled error
379    print $EVAL_ERROR;
380    exit ERROR_UNHANDLED;
381};
382
383# It shouldn't be possible to get here
384&log(ASSERT, 'execution reached invalid location in ' . __FILE__ . ', line ' . __LINE__);
385exit ERROR_ASSERT;
386