ansible/hacking/build_library/build_ansible/command_plugins/docs_build.py

234 lines
9 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
# Check if it contains any other .deps files than ancestor.deps
deps_files = [
df for df in glob.glob(os.path.join(directory_name, '*.deps'))
if os.path.basename(df) != 'ancestor.deps']
if not deps_files:
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