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