ansible/hacking/build_library/build_ansible/command_plugins/docs_build.py
Sandra McCann ccbfdec334
Split Ansible docs from core docs (#73616)
* excludes scenario guides from core docs, splits porting guides and roadmaps, symlinks indices to create index.html pages, and adds .gitignore entries for conf.py and the toplevel index.rst files generated by the docs build

This solution builds three types of docs:
* ansible-2.10 and earlier: all the docs.  Handle this via `make webdocs
  ANSIBLE_VERSION=2.10`
* ansible-3 and later: a subset of the docs for the ansible package.
  Handle this via `make webdocs ANSIBLE_VERSION=3` (change the
  ANSIBLE_VERSION to match the version being built for.
* ansible-core: a subset of the docs for the ansible-core package.
  Handle this via `make coredocs`.

* `make webdocs` now always builds all the collection docs
*  Use `make coredocs` to limit it to core plugins only
*  The user specifies the desired version. If no ANSIBLE_VERSION is specified, build plugins for the latest release of ansible
 
Co-authored-by: Toshio Kuratomi <a.badger@gmail.com>
Co-authored-by: Matt Clay <matt@mystile.com>
2021-02-17 09:57:05 -06:00

226 lines
8.7 KiB
Python

# coding: utf-8
# Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
import glob
import os
import os.path
import pathlib
import shutil
from tempfile import TemporaryDirectory
import yaml
from ansible.release import __version__ as ansible_base__version__
# Pylint doesn't understand Python3 namespace modules.
# pylint: disable=relative-beyond-top-level
from ..commands import Command
from ..errors import InvalidUserInput, MissingUserInput
# pylint: enable=relative-beyond-top-level
__metaclass__ = type
DEFAULT_TOP_DIR = pathlib.Path(__file__).parents[4]
DEFAULT_OUTPUT_DIR = pathlib.Path(__file__).parents[4] / 'docs/docsite'
class NoSuchFile(Exception):
"""An expected file was not found."""
#
# Helpers
#
def find_latest_ansible_dir(build_data_working):
"""Find the most recent ansible major version."""
# imports here so that they don't cause unnecessary deps for all of the plugins
from packaging.version import InvalidVersion, Version
ansible_directories = glob.glob(os.path.join(build_data_working, '[0-9.]*'))
# Find the latest ansible version directory
latest = None
latest_ver = Version('0')
for directory_name in (d for d in ansible_directories if os.path.isdir(d)):
try:
new_version = Version(os.path.basename(directory_name))
except InvalidVersion:
continue
if new_version > latest_ver:
latest_ver = new_version
latest = directory_name
if latest is None:
raise NoSuchFile('Could not find an ansible data directory in {0}'.format(build_data_working))
return latest
def find_latest_deps_file(build_data_working, ansible_version):
"""Find the most recent ansible deps file for the given ansible major version."""
# imports here so that they don't cause unnecessary deps for all of the plugins
from packaging.version import Version
data_dir = os.path.join(build_data_working, ansible_version)
deps_files = glob.glob(os.path.join(data_dir, '*.deps'))
if not deps_files:
raise Exception('No deps files exist for version {0}'.format(ansible_version))
# Find the latest version of the deps file for this major version
latest = None
latest_ver = Version('0')
for filename in deps_files:
with open(filename, 'r') as f:
deps_data = yaml.safe_load(f.read())
new_version = Version(deps_data['_ansible_version'])
if new_version > latest_ver:
latest_ver = new_version
latest = filename
if latest is None:
raise NoSuchFile('Could not find an ansible deps file in {0}'.format(data_dir))
return latest
#
# Subcommand base
#
def generate_base_docs(args):
"""Regenerate the documentation for all plugins listed in the plugin_to_collection_file."""
# imports here so that they don't cause unnecessary deps for all of the plugins
from antsibull.cli import antsibull_docs
with TemporaryDirectory() as tmp_dir:
#
# Construct a deps file with our version of ansible_base in it
#
modified_deps_file = os.path.join(tmp_dir, 'ansible.deps')
# The _ansible_version doesn't matter since we're only building docs for base
deps_file_contents = {'_ansible_version': ansible_base__version__,
'_ansible_base_version': ansible_base__version__}
with open(modified_deps_file, 'w') as f:
f.write(yaml.dump(deps_file_contents))
# Generate the plugin rst
return antsibull_docs.run(['antsibull-docs', 'stable', '--deps-file', modified_deps_file,
'--ansible-base-source', str(args.top_dir),
'--dest-dir', args.output_dir])
# If we make this more than just a driver for antsibull:
# Run other rst generation
# Run sphinx build
#
# Subcommand full
#
def generate_full_docs(args):
"""Regenerate the documentation for all plugins listed in the plugin_to_collection_file."""
# imports here so that they don't cause unnecessary deps for all of the plugins
import sh
from antsibull.cli import antsibull_docs
with TemporaryDirectory() as tmp_dir:
sh.git(['clone', 'https://github.com/ansible-community/ansible-build-data'], _cwd=tmp_dir)
# If we want to validate that the ansible version and ansible-base branch version match,
# this would be the place to do it.
build_data_working = os.path.join(tmp_dir, 'ansible-build-data')
ansible_version = args.ansible_version
if ansible_version is None:
ansible_version = find_latest_ansible_dir(build_data_working)
latest_filename = find_latest_deps_file(build_data_working, ansible_version)
# Make a copy of the deps file so that we can set the ansible-base version we'll use
modified_deps_file = os.path.join(tmp_dir, 'ansible.deps')
shutil.copyfile(latest_filename, modified_deps_file)
# Put our version of ansible-base into the deps file
with open(modified_deps_file, 'r') as f:
deps_data = yaml.safe_load(f.read())
deps_data['_ansible_base_version'] = ansible_base__version__
with open(modified_deps_file, 'w') as f:
f.write(yaml.dump(deps_data))
# Generate the plugin rst
return antsibull_docs.run(['antsibull-docs', 'stable', '--deps-file', modified_deps_file,
'--ansible-base-source', str(args.top_dir),
'--dest-dir', args.output_dir])
# If we make this more than just a driver for antsibull:
# Run other rst generation
# Run sphinx build
class CollectionPluginDocs(Command):
name = 'docs-build'
_ACTION_HELP = """Action to perform.
full: Regenerate the rst for the full ansible website.
base: Regenerate the rst for plugins in ansible-base and then build the website.
named: Regenerate the rst for the named plugins and then build the website.
"""
@classmethod
def init_parser(cls, add_parser):
parser = add_parser(cls.name,
description='Generate documentation for plugins in collections.'
' Plugins in collections will have a stub file in the normal plugin'
' documentation location that says the module is in a collection and'
' point to generated plugin documentation under the collections/'
' hierarchy.')
# I think we should make the actions a subparser but need to look in git history and see if
# we tried that and changed it for some reason.
parser.add_argument('action', action='store', choices=('full', 'base', 'named'),
default='full', help=cls._ACTION_HELP)
parser.add_argument("-o", "--output-dir", action="store", dest="output_dir",
default=DEFAULT_OUTPUT_DIR,
help="Output directory for generated doc files")
parser.add_argument("-t", "--top-dir", action="store", dest="top_dir",
default=DEFAULT_TOP_DIR,
help="Toplevel directory of this ansible-base checkout or expanded"
" tarball.")
parser.add_argument("-l", "--limit-to-modules", '--limit-to', action="store",
dest="limit_to", default=None,
help="Limit building module documentation to comma-separated list of"
" plugins. Specify non-existing plugin name for no plugins.")
parser.add_argument('--ansible-version', action='store',
dest='ansible_version', default=None,
help='The version of the ansible package to make documentation for.'
' This only makes sense when used with full.')
@staticmethod
def main(args):
# normalize and validate CLI args
if args.ansible_version and args.action != 'full':
raise InvalidUserInput('--ansible-version is only for use with "full".')
if not args.output_dir:
args.output_dir = os.path.abspath(str(DEFAULT_OUTPUT_DIR))
if args.action == 'full':
return generate_full_docs(args)
if args.action == 'base':
return generate_base_docs(args)
# args.action == 'named' (Invalid actions are caught by argparse)
raise NotImplementedError('Building docs for specific files is not yet implemented')
# return 0