#!/usr/bin/env python3 """ build.py -- Generate LVGL documentation using Doxygen and Sphinx + Breathe. Synopsis -------- - $ python build.py html [ skip_api ] [ fresh_env ] - $ python build.py latex [ skip_api ] [ fresh_env ] - $ python build.py intermediate [ skip_api ] - $ python build.py clean - $ python build.py clean_intermediate - $ python build.py clean_html - $ python build.py clean_latex Build Arguments and Clean Arguments can be used one at a time or be freely mixed and combined. Description ----------- Copy source files to an intermediate directory and modify them there before doc generation occurs. If a full rebuild is being done (e.g. after a `clean`) run Doxygen LVGL's source files to generate intermediate API information in XML format. Generate API documents for Breathe's consumption. Add API links to end of some documents. Generate example documents. From there, Sphinx with Breathe extension uses the resulting set of intermediate files to generate the desired output. It is only during this first build that the `skip_api` option has meaning. After the first build, no further actions is taken regarding API pages since they are not regenerated after the first build. The intermediate directory has a fixed location (overridable by `LVGL_DOC_BUILD_INTERMEDIATE_DIR` environment variable) and by default this script attempts to rebuild only those documents whose path, name or modification date has changed since the last build. The output directory also has a fixed location (overridable by `LVGL_DOC_BUILD_OUTPUT_DIR` environment variable). Caution: The document build meant for end-user consumption should ONLY be done after a `clean` unless you know that no API documentation and no code examples have changed. A `sphinx-build` will do a full doc rebuild any time: - the intermediate directory doesn't exist or is empty (since the new files in the intermediate directory will have modification times after the generated HTML or Latex files, even if nothing changed), - the targeted output directory doesn't exist or is empty, or - Sphinx determines that a full rebuild is necessary. This happens when: - intermediate directory (Sphinx's source-file path) has changed, - any options on the `sphinx-build` command line have changed, - `conf.py` modification date has changed, or - `fresh_env` argument is included (runs `sphinx-build` with -E option). Typical run time: Full build: 22.5 min skip_api : 1.9 min (applies to first build only) Options ------- help Print usage note and exit with status 0. html [ skip_api ] [ fresh_env ] Build HTML output. `skip_api` only has effect on first build after a `clean` or `clean_intermediate`. latex [ skip_api ] [ fresh_env ] Build Latex/PDF output (on hold pending removal of non-ASCII characters from input files). `skip_api` only has effect on first build after a `clean` or `clean_intermediate`. intermediate [ skip_api ] Generate intermediate directory contents (ready to build output formats). If they already exist, they are removed and re-generated. Note: "intermediate" can be abbreviated down to "int". skip_api (with `html` and/or `latex` and/or `intermediate` options) Skip API pages and links when intermediate directory contents are being generated (saving about 91% of build time). Note: they are not thereafter regenerated unless requested by `intermediate` argument or the intermediate directory does not exist. This is intended to be used only during doc development to speed up turn-around time between doc modifications and seeing final results. fresh_env (with `html` and/or `latex` options) Run `sphinx-build` with -E command-line argument, which makes it regenerate its "environment" (memory of what was built previously, forcing a full rebuild). clean Remove all generated files. clean_intermediate Remove intermediate directory. Note: "clean_intermediate" can be abbreviated down to "clean_int". clean_html Remove HTML output directory. clean_latex Remove Latex output directory. Unrecognized arguments print error message, usage note, and exit with status 1. Python Package Requirements --------------------------- The list of Python package requirements are in `requirements.txt`. Install them by: $ pip install -r requirements.txt History ------- The first version of this file (Apr 2021) discovered the name of the current branch (e.g. 'master', 'release/v8.4', etc.) to support different versions of the documentation by establishing the base URL (used in `conf.py` and in [Edit on GitHub] links), and then ran: - Doxygen (to generate LVGL API XML), then - Sphinx to generate the LVGL document tree. Internally, Sphinx uses `breathe` (a Sphinx extension) to provide a bridge between Doxygen XML output and Sphinx documentation. It also supported a command-line option `clean` to remove generated files before starting (eliminates orphan files, for docs that have moved or changed). Since then its duties have grown to include: - Using environment variables to convey branch names to several more places where they are used in the docs-generating process (instead of re-writing writing `conf.py` and a `header.rst` each time docs were generated). These are documented where they generated below. - Supporting additional command-line options. - Generating a `./docs/lv_conf.h` for Doxygen to use (config_builder.py). - Supporting multiple execution platforms (which then required tokenizing Doxygen's INPUT path in `Doxyfile` and re-writing portions that used `sed` to generate input or modify files). - Adding translation and API links (requiring generating docs in an intermediate directory so that the links could be programmatically added to each document before Sphinx was run). Note: translation link are now done manually since they are only on the main landing page. - Generating EXAMPLES page + sub-examples where applicable to individual documents, e.g. to widget-, style-, layout-pages, etc. - Building PDF via latex (when working). - Shifting doc-generation paradigm to behave more like `make`. """ # **************************************************************************** # IMPORTANT: If you are getting a PDF-lexer error for an example, check # for extra lines at the end of the file. Only a single empty line # is allowed!!! Ask me how long it took me to figure this out. # -- @kdschlosser # **************************************************************************** # Python Library import sys import os import subprocess import shutil import dirsync from datetime import datetime # LVGL Custom import example_list import doc_builder import config_builder _ = os.path.abspath(os.path.dirname(__file__)) docs_src_dir = os.path.join(_, 'src') sys.path.insert(0, docs_src_dir) from lvgl_version import lvgl_version # NoQA # Not Currently Used # (Code is kept in case we want to re-implement it later.) # import add_translation # ------------------------------------------------------------------------- # Configuration # ------------------------------------------------------------------------- # These are relative paths from the ./docs/ directory. cfg_project_dir = '..' cfg_src_dir = 'src' cfg_examples_dir = 'examples' cfg_default_intermediate_dir = 'intermediate' cfg_default_output_dir = 'build' cfg_static_dir = '_static' cfg_downloads_dir = 'downloads' cfg_lv_conf_filename = 'lv_conf.h' cfg_lv_version_filename = 'lv_version.h' cfg_doxyfile_filename = 'Doxyfile' cfg_top_index_filename = 'index.rst' # Filename generated in `latex_output_dir` and copied to `pdf_output_dir`. cfg_pdf_filename = 'LVGL.pdf' # ------------------------------------------------------------------------- # Print usage note. # ------------------------------------------------------------------------- def print_usage_note(): print('Usage:') print(' $ python build.py [optional_arg ...]') print() print(' where `optional_arg` can be any of these:') print(' html [ skip_api ] [ fresh_env ]') print(' latex [ skip_api ] [ fresh_env ]') print(' intermediate [ skip_api ]') print(' clean') print(' clean_intermediate') print(' clean_html') print(' clean_latex') print(' help') def remove_dir(tgt_dir): """Remove directory `tgt_dir`.""" if os.path.isdir(tgt_dir): print(f'Removing {tgt_dir}...') shutil.rmtree(tgt_dir) else: print(f'{tgt_dir} already removed...') def cmd(s, start_dir=None, exit_on_error=True): """Run external command and abort build on error.""" if start_dir is None: start_dir = os.getcwd() saved_dir = os.getcwd() os.chdir(start_dir) print("") print(s) print("-------------------------------------") result = os.system(s) os.chdir(saved_dir) if result != 0 and exit_on_error: print("Exiting build due to previous error.") sys.exit(result) def intermediate_dir_contents_exists(dir): """Provide answer to question: Can we have reasonable confidence that the contents of `intermediate_directory` already exists? """ result = False c1 = os.path.isdir(dir) if c1: temp_path = os.path.join(dir, 'CHANGELOG.rst') c2 = os.path.exists(temp_path) temp_path = os.path.join(dir, '_ext') c3 = os.path.isdir(temp_path) temp_path = os.path.join(dir, '_static') c4 = os.path.isdir(temp_path) temp_path = os.path.join(dir, 'details') c5 = os.path.isdir(temp_path) temp_path = os.path.join(dir, 'intro') c6 = os.path.isdir(temp_path) temp_path = os.path.join(dir, 'contributing') c7 = os.path.isdir(temp_path) temp_path = os.path.join(dir, cfg_examples_dir) c8 = os.path.isdir(temp_path) result = c2 and c3 and c4 and c5 and c6 and c7 and c8 return result def run(args): """Perform doc-build function(s) requested.""" # --------------------------------------------------------------------- # Set default settings. # --------------------------------------------------------------------- build_html = False build_latex = False build_intermediate = False skip_api = False fresh_sphinx_env = False clean_all = False clean_intermediate = False clean_html = False clean_latex = False def print_setting(setting_name, val): """Print one setting; used for debugging.""" print(f'{setting_name:18} = [{val}]') def print_settings(and_exit): """Print all settings and optionally exit; used for debugging.""" print_setting("build_html", build_html) print_setting("build_latex", build_latex) print_setting("build_intermediate", build_intermediate) print_setting("skip_api", skip_api) print_setting("fresh_sphinx_env", fresh_sphinx_env) print_setting("clean_all", clean_all) print_setting("clean_intermediate", clean_intermediate) print_setting("clean_html", clean_html) print_setting("clean_latex", clean_latex) if and_exit: exit(0) # --------------------------------------------------------------------- # Process args. # --------------------------------------------------------------------- # No args means print usage note and exit with status 0. if len(args) == 0: print_usage_note() exit(0) # Some args are present. Interpret them in loop here. # Unrecognized arg means print error, print usage note, exit with status 1. for arg in args: # We use chained `if-elif-else` instead of `match` for those on Linux # systems that will not have the required version 3.10 of Python yet. if arg == 'help': print_usage_note() exit(0) elif arg == "html": build_html = True elif arg == "latex": build_latex = True elif "intermediate".startswith(arg) and len(arg) >= 3: # Accept abbreviations. build_intermediate = True elif arg == 'skip_api': skip_api = True elif arg == 'fresh_env': fresh_sphinx_env = True elif arg == "clean": clean_all = True clean_intermediate = True clean_html = True clean_latex = True elif arg == "clean_html": clean_html = True elif arg == "clean_latex": clean_latex = True elif "clean_intermediate".startswith(arg) and len(arg) >= 9: # Accept abbreviations. # Needs to be after others so "cl" will not clean_intermediate = True else: print(f'Argument [{arg}] not recognized.') print() print_usage_note() exit(1) # '-E' option forces Sphinx to rebuild its environment so all docs are # fully regenerated, even if not changed. # Note: Sphinx runs in ./docs/, but uses `intermediate_dir` for input. if fresh_sphinx_env: print("Force-regenerating all files...") env_opt = '-E' else: env_opt = '' # --------------------------------------------------------------------- # Start. # --------------------------------------------------------------------- t0 = datetime.now() # --------------------------------------------------------------------- # Set up paths. # # Variable Suffixes: # _filename = filename without path # _path = path leading to a file or directory (absolute or relative) # _file = path leading to a file (absolute or relative) # _dir = path leading to a directory (absolute or relative) # --------------------------------------------------------------------- base_dir = os.path.abspath(os.path.dirname(__file__)) project_dir = os.path.abspath(os.path.join(base_dir, cfg_project_dir)) examples_dir = os.path.join(project_dir, cfg_examples_dir) lvgl_src_dir = os.path.join(project_dir, 'src') # Establish intermediate directory. The presence of environment variable # `LVGL_DOC_BUILD_INTERMEDIATE_DIR` overrides default in `cfg_default_intermediate_dir`. if 'LVGL_DOC_BUILD_INTERMEDIATE_DIR' in os.environ: intermediate_dir = os.environ['LVGL_DOC_BUILD_INTERMEDIATE_DIR'] else: intermediate_dir = os.path.join(base_dir, cfg_default_intermediate_dir) lv_conf_file = os.path.join(intermediate_dir, cfg_lv_conf_filename) version_dst_file = os.path.join(intermediate_dir, cfg_lv_version_filename) top_index_file = os.path.join(intermediate_dir, cfg_top_index_filename) doxyfile_src_file = os.path.join(base_dir, cfg_doxyfile_filename) doxyfile_dst_file = os.path.join(intermediate_dir, cfg_doxyfile_filename) pdf_intermediate_dst_dir = os.path.join(intermediate_dir, cfg_static_dir, cfg_downloads_dir) pdf_intermediate_dst_file = os.path.join(pdf_intermediate_dst_dir, cfg_pdf_filename) sphinx_path_sep = '/' pdf_relative_file = cfg_static_dir + sphinx_path_sep + cfg_downloads_dir + sphinx_path_sep + cfg_pdf_filename pdf_link_ref_str = f'PDF Version: :download:`{cfg_pdf_filename} <{pdf_relative_file}>`' # Establish build directory. The presence of environment variable # `LVGL_DOC_BUILD_OUTPUT_DIR` overrides default in `cfg_default_output_dir`. if 'LVGL_DOC_BUILD_OUTPUT_DIR' in os.environ: output_dir = os.environ['LVGL_DOC_BUILD_OUTPUT_DIR'] else: output_dir = os.path.join(base_dir, cfg_default_output_dir) html_output_dir = os.path.join(output_dir, 'html') latex_output_dir = os.path.join(output_dir, 'latex') pdf_output_dir = os.path.join(output_dir, 'pdf') pdf_src_file = os.path.join(latex_output_dir, cfg_pdf_filename) pdf_dst_file = os.path.join(pdf_output_dir, cfg_pdf_filename) version_src_file = os.path.join(project_dir, cfg_lv_version_filename) # Special stuff for right-aligning PDF download link. # Note: this needs to be embedded in a
tag # and in HTML5,
tags cannot be nested! cfg_right_just_para_text = """.. raw:: html