1#!/usr/bin/env python3
2""" Generate LVGL documentation using Doxygen and Sphinx.
3
4The first version of this file (Apr 2021) discovered the name of
5the current branch (e.g. 'master', 'release/v8.4', etc.) to support
6different versions of the documentation by establishing the base URL
7(used in `conf.py` and in [Edit on GitHub] links), and then ran:
8
9- Doxygen (to generate LVGL API XML), then
10- Sphinx
11
12to generate the LVGL document tree.  Internally, Sphinx uses `breathe`
13(a Sphinx extension) to provide a bridge between Doxygen XML output and
14Sphinx documentation.  It also supported a command-line option `clean`
15to remove generated files before starting (eliminates orphan files,
16for docs that have moved or changed).
17
18Since then its duties have grown to include:
19
20- Using environment variables to convey branch names to several more
21  places where they are used in the docs-generating process (instead
22  of re-writing writing `conf.py` and a `header.rst` each time docs
23  were generated).  These are documented where they generated below.
24
25- Supporting additional command-line options.
26
27- Generating a temporary `./docs/lv_conf.h` for Doxygen to use
28  (config_builder.py).
29
30- Supporting multiple execution platforms (which then required tokenizing
31  Doxygen's INPUT path in `Doxyfile` and re-writing portions that used
32  `sed` to generate input or modify files).
33
34- Adding translation and API links (requiring generating docs in a
35  temporary directory so that the links could be programmatically
36  added to each document before Sphinx was run).
37
38- Generating EXAMPLES page + sub-examples where applicable to individual
39  documents, e.g. to widget-, style-, layout-pages, etc.
40
41- Building PDF via latex (when working).
42
43
44Command-Line Arguments
45----------------------
46Command-line arguments have been broken down to give the user the
47ability to control each individual major variation in behavior of
48this script.  These were added to speed up long doc-development
49tasks by shortening the turn-around time between doc modification
50and seeing the final .html results in a local development environment.
51Finally, this script can now be used in a way such that Sphinx will
52only modify changed documents, and reduce an average ~22-minute
53run time to a run time that is workable for rapidly repeating doc
54generation to see Sphinx formatting results quickly.
55
56
57Normal Usage
58------------
59This is the way this script is used for normal (full) docs generation.
60
61    $ python  build.py  skip_latex
62
63
64Docs-Dev Initial Docs Generation Usage
65--------------------------------------
661.  Set `LVGL_FIXED_TEMP_DIR` environment variable to path to
67    the temporary directory you will use over and over during
68    document editing, without trailing directory separator.
69    Initially directory should not exist.
70
712.  $ python  build.py  skip_latex  preserve  fixed_tmp_dir
72
73This takes typically ~22 minutes.
74
75
76Docs-Dev Update-Only Generation Usage
77-------------------------------------
78After the above has been run through once, you can thereafter
79run the following until docs-development task is complete.
80
81    $ python  build.py  skip_latex  docs_dev  update
82
83Generation time depends on the number of `.rst` files that
84have been updated:
85
86+--------------+------------+---------------------------------+
87| Docs Changed | Time       | Typical Time to Browser Refresh |
88+==============+============+=================================+
89|  0           |  6 seconds |             n/a                 |
90+--------------+------------+---------------------------------+
91|  1           | 19 seconds |          12 seconds             |
92+--------------+------------+---------------------------------+
93|  5           | 28 seconds |          21 seconds             |
94+--------------+------------+---------------------------------+
95| 20           | 59 seconds |          52 seconds             |
96+--------------+------------+---------------------------------+
97
98
99Sphinx Doc-Regeneration Criteria
100--------------------------------
101Sphinx uses the following to determine what documents get updated:
102
103- source-doc modification date
104  - Change the modification date and `sphinx-build` will re-build it.
105
106- full (absolute) path to the source document, including its file name
107  - Change the path or filename and `sphinx-build` will re-build it.
108
109- whether the -E option is on the `sphinx-build` command line
110  - -E forces `sphinx-build` to do a full re-build.
111
112
113Argument Descriptions
114---------------------
115- skip_latex
116    The meaning of this argument has not changed:  it simply skips
117    attempting to generate Latex and subsequent PDF generation.
118- skip_api
119    Skips generating API pages (this saves about 70% of build time).
120    This is intended to be used only during doc development to speed up
121    turn-around time between doc modifications and seeing final results.
122- no_fresh_env
123    Excludes -E command-line argument to `sphinx-build`, which, when present,
124    forces it to generate a whole new environment (memory of what was built
125    previously, forcing a full rebuild).  "no_fresh_env" enables a rebuild
126    of only docs that got updated -- Sphinx's default behavior.
127- preserve (previously "develop")
128    Leaves temporary directory intact for docs development purposes.
129- fixed_tmp_dir
130    If (fixed_tmp_dir and 'LVGL_FIXED_TEMP_DIR' in `os.environ`),
131    then this script uses the value of that that environment variable
132    to populate `temp_directory` instead of the normal (randomly-named)
133    temporary directory.  This is important when getting `sphinx-build`
134    to ONLY rebuild updated documents, since changing the directory
135    from which they are generated (normally the randomly-named temp
136    dir) will force Sphinx to do a full-rebuild because it remembers
137    the doc paths from which the build was last performed.
138- skip_trans
139    Skips adding translation links.  This allows direct copying of
140    `.rst` files to `temp_directory` when they are updated to save time
141    during re-build.  Final build must not include this option so that
142    the translation links are added at the top of each intended page.
143- no_copy
144    Skips copying ./docs/ directory tree to `temp_directory`.
145    This is only honored if:
146    - fixed_tmp_dir == True, and
147    - the doc files were previously copied to the temporary directory
148      and thus are already present there.
149- docs_dev
150    This is a command-line shortcut to combining these command-line args:
151    - no_fresh_env
152    - preserve
153    - fixed_tmp_dir
154    - no_copy
155- update
156    When no_copy is active, check modification dates on `.rst` files
157    and re-copy the updated `./docs/` files to the temporary directory
158    that have later modification dates, thus updating what Sphinx uses
159    as input.
160    Warning:  this wipes out translation links and API-page links that
161    were added in the first pass, so should only be used for doc
162    development -- not for final doc generation.
163"""
164
165# ****************************************************************************
166# IMPORTANT: If you are getting a PDF-lexer error for an example, check
167#            for extra lines at the end of the file. Only a single empty line
168#            is allowed!!! Ask me how long it took me to figure this out.
169# ****************************************************************************
170
171
172def run():
173    # Python Library Imports
174    import sys
175    import os
176    import re
177    import subprocess
178    import shutil
179    import tempfile
180    import dirsync
181    from datetime import datetime
182
183    # LVGL Custom Imports
184    import example_list as ex
185    import doc_builder
186    import config_builder
187    import add_translation
188
189    # ---------------------------------------------------------------------
190    # Start.
191    # ---------------------------------------------------------------------
192    t1 = datetime.now()
193    print('Current time:  ' + str(t1))
194
195    # ---------------------------------------------------------------------
196    # Process args.
197    #
198    # With argument `docs_dev`, Sphinx will generate docs from a fixed
199    # temporary directory that can be then used later using the same
200    # command line to get Sphinx to ONLY rebuild changed documents.
201    # This saves a huge amount of time during long document projects.
202    # ---------------------------------------------------------------------
203    # Set defaults.
204    clean = False
205    skip_latex = False
206    skip_api = False
207    fresh_sphinx_env = True
208    preserve = False
209    fixed_tmp_dir = False
210    skip_trans = False
211    no_copy = False
212    docs_dev = False
213    update = False
214    args = sys.argv[1:]
215
216    for arg in args:
217        # We use chained `if-elif-else` instead of `match` for those on Linux
218        # systems that will not have the required version 3.10 of Python yet.
219        if arg == "clean":
220            clean = True
221        elif arg == "skip_latex":
222            skip_latex = True
223        elif arg == 'skip_api':
224            skip_api = True
225        elif arg == 'no_fresh_env':
226            fresh_sphinx_env = False
227        elif arg == 'preserve':
228            preserve = True
229        elif arg == 'fixed_tmp_dir':
230            fixed_tmp_dir = True
231        elif arg == 'skip_trans':
232            skip_trans = True
233        elif arg == 'no_copy':
234            no_copy = True
235        elif arg == 'docs_dev':
236            docs_dev = True
237        elif arg == 'update':
238            update = True
239        else:
240            print(f'Argument [{arg}] not recognized.')
241            exit(1)
242
243    # Arg ramifications:
244    # docs_dev implies no_fresh_env, preserve, fixed_tmp_dir, and no_copy.
245    if docs_dev:
246        fresh_sphinx_env = False
247        preserve = True
248        fixed_tmp_dir = True
249        no_copy = True
250
251    # ---------------------------------------------------------------------
252    # Due to the modifications that take place to the documentation files
253    # when the documentation builds it is better to copy the source files to a
254    # temporary folder and modify the copies. Not setting it up this way makes it
255    # a real headache when making alterations that need to be committed as the
256    # alterations trigger the files as changed.  Also, this keeps maintenance
257    # effort to a minimum as adding a new language translation only needs to be
258    # done in 2 places (add_translation.py and ./docs/_ext/link_roles.py) rather
259    # than once for each .rst file.
260    #
261    # The html and PDF output locations are going to remain the same as they were.
262    # it's just the source documentation files that are going to be copied.
263    # ---------------------------------------------------------------------
264    if fixed_tmp_dir and 'LVGL_FIXED_TEMP_DIR' in os.environ:
265        temp_directory = os.environ['LVGL_FIXED_TEMP_DIR']
266    else:
267        temp_directory = tempfile.mkdtemp(suffix='.lvgl_docs')
268
269    print(f'Using temp directory:  [{temp_directory}]')
270
271    # ---------------------------------------------------------------------
272    # Set up paths.
273    # ---------------------------------------------------------------------
274    base_path = os.path.abspath(os.path.dirname(__file__))
275    project_path = os.path.abspath(os.path.join(base_path, '..'))
276    examples_path = os.path.join(project_path, 'examples')
277    lvgl_src_path = os.path.join(project_path, 'src')
278    latex_output_path = os.path.join(temp_directory, 'out_latex')
279    pdf_src_file = os.path.join(latex_output_path, 'LVGL.pdf')
280    pdf_dst_file = os.path.join(temp_directory, 'LVGL.pdf')
281    html_src_path = temp_directory
282    html_dst_path = os.path.join(project_path, 'out_html')
283
284    # ---------------------------------------------------------------------
285    # Change to script directory for consistency.
286    # ---------------------------------------------------------------------
287    os.chdir(base_path)
288
289    # ---------------------------------------------------------------------
290    # Provide a way to run an external command and abort build on error.
291    # ---------------------------------------------------------------------
292    def cmd(s, start_dir=None):
293        if start_dir is None:
294            start_dir = os.getcwd()
295
296        saved_dir = os.getcwd()
297        os.chdir(start_dir)
298        print("")
299        print(s)
300        print("-------------------------------------")
301        result = os.system(s)
302        os.chdir(saved_dir)
303
304        if result != 0:
305            print("Exiting build due to previous error.")
306            sys.exit(result)
307
308    # ---------------------------------------------------------------------
309    # Populate LVGL_URLPATH and LVGL_GITCOMMIT environment variables:
310    #   - LVGL_URLPATH   <= 'master' or '8.4' '9.2' etc.
311    #   - LVGL_GITCOMMIT <= same (see 03-Oct-2024 note below).
312    #
313    # These supply input later in the doc-generation process as follows:
314    #
315    # LVGL_URLPATH is used by:
316    #   - `conf.py` to build `html_baseurl` for Sphinx for
317    #       - generated index
318    #       - generated search window
319    #       - establishing canonical page for search engines
320    #   - `link_roles.py` to generate translation links
321    #   - `doc_builder.py` to generate links to API pages
322    #
323    # LVGL_GITCOMMIT is used by:
324    #   - `conf.py` => html_context['github_version'] for
325    #     Sphinx Read-the-Docs theme to add to [Edit on GitHub] links
326    #   - `conf.py` => repo_commit_hash for generated EXAMPLES pages for:
327    #       - [View on GitHub] buttons (view C code examples)
328    #       - [View on GitHub] buttons (view Python code examples)
329    # ---------------------------------------------------------------------
330    # 03-Oct-2024:  Gabor requested LVGL_GITCOMMIT be changed to a branch
331    # name since that will always be current, and it will fix a large
332    # number of broken links on the docs website, since commits that
333    # generated docs can sometimes go away.  This gets used in:
334    # - [Edit on GitHub] links in doc pages (via Sphinx theme), and
335    # - [View on GitHub] links in example pages (via `example_list.py`
336    #   and `lv_example.py`).
337    # Original code:
338    # status, br = subprocess.getstatusoutput("git branch --show-current")
339    # _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD")
340    # br = re.sub(r'\* ', '', br)
341    #   're' was previously used to remove leading '* ' from current branch
342    #   string when we were parsing output from bare `git branch` output.
343    #   This is no longer needed with `--show-current` option now used.
344    # ---------------------------------------------------------------------
345    status, branch = subprocess.getstatusoutput("git branch --show-current")
346
347    # If above failed (i.e. `branch` not valid), default to 'master'.
348    if status != 0:
349        branch = 'master'
350    elif branch == 'master':
351        # Expected in most cases.  Nothing to change.
352        pass
353    else:
354        # `branch` is valid.  Capture release version if in a 'release/' branch.
355        if branch.startswith('release/'):
356            branch = branch[8:]
357        else:
358            # Default to 'master'.
359            branch = 'master'
360
361    os.environ['LVGL_URLPATH'] = branch
362    os.environ['LVGL_GITCOMMIT'] = branch
363
364    # ---------------------------------------------------------------------
365    # Start doc-build process.
366    # ---------------------------------------------------------------------
367    print("")
368    print("****************")
369    print("Building")
370    print("****************")
371
372    # Remove all previous output files if 'clean' on command line.
373    if clean:
374        print('Removing previous output files...')
375        # The below commented-out code below is being preserved
376        # for docs-generation development purposes.
377
378        # api_path = os.path.join(temp_directory, 'API')
379        # xml_path = os.path.join(temp_directory, 'xml')
380        # doxy_path = os.path.join(temp_directory, 'doxygen_html')
381
382        # if os.path.exists(api_path):
383        #     shutil.rmtree(api_path)
384
385        # lang = 'en'
386        # if os.path.exists(lang):
387        #     shutil.rmtree(lang)
388
389        if os.path.exists(html_dst_path):
390            shutil.rmtree(html_dst_path)
391
392        # if os.path.exists(xml_path):
393        #     shutil.rmtree(xml_path)
394        #
395        # if os.path.exists(doxy_path):
396        #     shutil.rmtree(doxy_path)
397
398        # os.mkdir(api_path)
399        # os.mkdir(lang)
400
401    # ---------------------------------------------------------------------
402    # Build local lv_conf.h from lv_conf_template.h for this build only.
403    # ---------------------------------------------------------------------
404    config_builder.run()
405
406    # ---------------------------------------------------------------------
407    # Provide answer to question:  Can we have reasonable confidence that
408    # the contents of `temp_directory` already exists?
409    # ---------------------------------------------------------------------
410    def temp_dir_contents_exists():
411        result = False
412        c1 = os.path.exists(temp_directory)
413
414        if c1:
415            temp_path = os.path.join(temp_directory, 'CHANGELOG.rst')
416            c2 = os.path.exists(temp_path)
417            temp_path = os.path.join(temp_directory, 'CODING_STYLE.rst')
418            c3 = os.path.exists(temp_path)
419            temp_path = os.path.join(temp_directory, 'CONTRIBUTING.rst')
420            c4 = os.path.exists(temp_path)
421            temp_path = os.path.join(temp_directory, '_ext')
422            c5 = os.path.exists(temp_path)
423            temp_path = os.path.join(temp_directory, '_static')
424            c6 = os.path.exists(temp_path)
425            temp_path = os.path.join(temp_directory, 'details')
426            c7 = os.path.exists(temp_path)
427            temp_path = os.path.join(temp_directory, 'intro')
428            c8 = os.path.exists(temp_path)
429            temp_path = os.path.join(temp_directory, 'examples')
430            c9 = os.path.exists(temp_path)
431            result = c2 and c3 and c4 and c5 and c6 and c7 and c8 and c9
432
433        return result
434
435    # ---------------------------------------------------------------------
436    # Copy files to 'temp_directory' where they will be edited (translation
437    # link and API links) before being used to generate new docs.
438    # ---------------------------------------------------------------------
439    doc_files_copied = False
440    if no_copy and fixed_tmp_dir and temp_dir_contents_exists():
441        if update:
442            exclude_list = ['lv_conf.h']
443            options = {
444                'verbose': True,
445                'create': True,
446                'exclude': exclude_list
447            }
448            dirsync.sync('.', temp_directory, 'update', **options)
449        else:
450            print("Skipping copying ./docs/ directory as requested.")
451    else:
452        shutil.copytree('.', temp_directory, dirs_exist_ok=True)
453        shutil.copytree(examples_path, os.path.join(temp_directory, 'examples'), dirs_exist_ok=True)
454        doc_files_copied = True
455
456    # ---------------------------------------------------------------------
457    # Replace tokens in Doxyfile in 'temp_directory' with data from this run.
458    # ---------------------------------------------------------------------
459    if doc_files_copied:
460        with open(os.path.join(temp_directory, 'Doxyfile'), 'rb') as f:
461            data = f.read().decode('utf-8')
462
463        data = data.replace('#*#*LV_CONF_PATH*#*#', os.path.join(base_path, 'lv_conf.h'))
464        data = data.replace('*#*#SRC#*#*', '"{0}"'.format(lvgl_src_path))
465
466        with open(os.path.join(temp_directory, 'Doxyfile'), 'wb') as f:
467            f.write(data.encode('utf-8'))
468
469        # -----------------------------------------------------------------
470        # Generate examples pages.  Include sub-pages pages that get included
471        # in individual documents where applicable.
472        # -----------------------------------------------------------------
473        print("Generating examples...")
474        ex.exec(temp_directory)
475
476        # -----------------------------------------------------------------
477        # Add translation links.
478        # -----------------------------------------------------------------
479        if skip_trans:
480            print("Skipping translation links as requested.")
481        else:
482            print("Adding translation links...")
483            add_translation.exec(temp_directory)
484
485        # ---------------------------------------------------------------------
486        # Generate API pages and links thereto.
487        # ---------------------------------------------------------------------
488        if skip_api:
489            print("Skipping API generation as requested.")
490        else:
491            print("Running Doxygen...")
492            cmd('doxygen Doxyfile', temp_directory)
493
494            doc_builder.EMIT_WARNINGS = False
495
496            # Create .RST files for API pages.
497            doc_builder.run(
498                project_path,
499                temp_directory,
500                os.path.join(temp_directory, 'intro'),
501                os.path.join(temp_directory, 'intro', 'add-lvgl-to-your-project'),
502                os.path.join(temp_directory, 'details'),
503                os.path.join(temp_directory, 'details', 'base-widget'),
504                os.path.join(temp_directory, 'details', 'base-widget', 'layouts'),
505                os.path.join(temp_directory, 'details', 'base-widget', 'styles'),
506                os.path.join(temp_directory, 'details', 'debugging'),
507                os.path.join(temp_directory, 'details', 'integration'),
508                os.path.join(temp_directory, 'details', 'integration', 'bindings'),
509                os.path.join(temp_directory, 'details', 'integration', 'building'),
510                os.path.join(temp_directory, 'details', 'integration', 'chip'),
511                os.path.join(temp_directory, 'details', 'integration', 'driver'),
512                os.path.join(temp_directory, 'details', 'integration', 'driver', 'display'),
513                os.path.join(temp_directory, 'details', 'integration', 'driver', 'touchpad'),
514                os.path.join(temp_directory, 'details', 'integration', 'framework'),
515                os.path.join(temp_directory, 'details', 'integration', 'ide'),
516                os.path.join(temp_directory, 'details', 'integration', 'os'),
517                os.path.join(temp_directory, 'details', 'integration', 'os', 'yocto'),
518                os.path.join(temp_directory, 'details', 'integration', 'renderers'),
519                os.path.join(temp_directory, 'details', 'libs'),
520                os.path.join(temp_directory, 'details', 'main-components'),
521                os.path.join(temp_directory, 'details', 'other-components'),
522                os.path.join(temp_directory, 'details', 'widgets')
523            )
524
525            print('Reading Doxygen output...')
526
527    # ---------------------------------------------------------------------
528    # BUILD PDF
529    # ---------------------------------------------------------------------
530    if skip_latex:
531        print("Skipping latex build as requested.")
532    else:
533        # Remove PDF link so PDF does not have a link to itself.
534        index_path = os.path.join(temp_directory, 'index.rst')
535
536        with open(index_path, 'rb') as f:
537            index_data = f.read().decode('utf-8')
538
539        # Support both Windows and Linux platforms with `os.linesep`.
540        pdf_link_ref_str = 'PDF version: :download:`LVGL.pdf <LVGL.pdf>`' + os.linesep
541        if pdf_link_ref_str in index_data:
542            index_data = index_data.replace(pdf_link_ref_str, '')
543
544            with open(index_path, 'wb') as f:
545                f.write(index_data.encode('utf-8'))
546
547        # Silly workaround to include the more or less correct
548        # PDF download link in the PDF
549        # cmd("cp -f " + lang +"/latex/LVGL.pdf LVGL.pdf | true")
550        src = temp_directory
551        dst = latex_output_path
552        cpu = os.cpu_count()
553        cmd_line = f'sphinx-build -b latex "{src}" "{dst}" -j {cpu}'
554        cmd(cmd_line)
555
556        # Generate PDF.
557        cmd_line = 'latexmk -pdf "LVGL.tex"'
558        cmd(cmd_line, latex_output_path)
559
560        # Copy the result PDF to the main directory to make
561        # it available for the HTML build.
562        shutil.copyfile(pdf_src_file, pdf_dst_file)
563
564        # Add PDF link back in so HTML build will have it.
565        index_data = pdf_link_ref_str + index_data
566
567        with open(index_path, 'wb') as f:
568            f.write(index_data.encode('utf-8'))
569
570    # ---------------------------------------------------------------------
571    # BUILD HTML
572    # ---------------------------------------------------------------------
573    # This version of get_version() works correctly under both Linux and Windows.
574    # Updated to be resilient to changes in `lv_version.h` compliant with C macro syntax.
575    def get_version():
576        path = os.path.join(project_path, 'lv_version.h')
577        major = ''
578        minor = ''
579
580        with open(path, 'r') as file:
581            major_re = re.compile(r'define\s+LVGL_VERSION_MAJOR\s+(\d+)')
582            minor_re = re.compile(r'define\s+LVGL_VERSION_MINOR\s+(\d+)')
583
584            for line in file.readlines():
585                # Skip if line not long enough to match.
586                if len(line) < 28:
587                    continue
588
589                match = major_re.search(line)
590                if match is not None:
591                    major = match[1]
592                else:
593                    match = minor_re.search(line)
594                    if match is not None:
595                        minor = match[1]
596                        # Exit early if we have both values.
597                        if len(major) > 0 and len(minor) > 0:
598                            break
599
600        return f'{major}.{minor}'
601
602    # Note:  While it can be done (e.g. if one needs to set a stop point
603    # in Sphinx code for development purposes), it is NOT a good idea to
604    # run Sphinx from script as
605    #   from sphinx.cmd.build import main as sphinx_build
606    #   sphinx_args = [...]
607    #   sphinx_build(sphinx_args)
608    # because it takes ~10X longer to run than `sphinx_build` executable.
609    # Literally > 3 hours.
610
611    # '-E' option forces Sphinx to rebuild its environment so all docs are
612    # fully regenerated, even if not changed.
613    # Note:  Sphinx runs in ./docs/, but uses `temp_directory` for input.
614    if fresh_sphinx_env:
615        print("Regenerating all files...")
616        env_opt = '-E'
617    else:
618        print("Regenerating only updated files...")
619        env_opt = ''
620
621    ver = get_version()
622    src = html_src_path
623    dst = html_dst_path
624    cpu = os.cpu_count()
625    cmd_line = f'sphinx-build -b html "{src}" "{dst}" -D version="{ver}" {env_opt} -j {cpu}'
626    t2 = datetime.now()
627    print('Current time:  ' + str(t2))
628    cmd(cmd_line)
629    t3 = datetime.now()
630    print('Current time:     ' + str(t3))
631    print('Sphinx run time:  ' + str(t3 - t2))
632
633    # ---------------------------------------------------------------------
634    # Cleanup.
635    # ---------------------------------------------------------------------
636    if preserve:
637        print('Temp directory:  ', temp_directory)
638    else:
639        print('Removing temporary files...', temp_directory)
640        if os.path.exists(temp_directory):
641            shutil.rmtree(temp_directory)
642
643    # ---------------------------------------------------------------------
644    # Remove temporary `lv_conf.h` created for this build.
645    # ---------------------------------------------------------------------
646    config_builder.cleanup()
647
648    # ---------------------------------------------------------------------
649    # Indicate results.
650    # ---------------------------------------------------------------------
651    t4 = datetime.now()
652    print('Total run time:   ' + str(t4 - t1))
653    print('Output path:     ', html_dst_path)
654    print()
655    print('Note:  warnings about `/details/index.rst` and `/intro/index.rst`')
656    print('       "not being in any toctree" are expected and intentional.')
657    print()
658    print('Finished.')
659
660
661# -------------------------------------------------------------------------
662# Make module importable as well as run-able.
663# -------------------------------------------------------------------------
664if __name__ == '__main__':
665    run()
666