[PATCH 5 of 7] setup: generate, build & install documentation using distutils & docutils
Dan Villiom Podlaski Christiansen
danchr at gmail.com
Tue Dec 1 20:33:15 UTC 2009
# HG changeset patch
# User Dan Villiom Podlaski Christiansen <danchr at gmail.com>
# Date 1259682048 -3600
# Node ID 2b45949ca5a918731ceaa20f4d6c46a89a9a7135
# Parent 4a4fa19cd99df0cbe82de93c79ce312fb4e7eb6e
setup: generate, build & install documentation using distutils & docutils.
Add a new dedicated build command for generating & installing the
documentation. Unfortunately, this stage is somewhat complex. The
motivation for this change is described in the source docstrings in
`setup.py'.
Add a `doc-format' option, which selects the documentation formats to
build. The defaults is to install both formats --- manual pages as
well as HTML --- on UNIX, and only HTML elsewhere.
Turn the `doc' directory into a package by adding an empty __init__.py
file. Also, rename `rst2man.py' to `manpage.py', reflecting its new
role.
Remove the Makefile in `doc', as it is now redundant. Strip out
references to it from the top-level Makefile.
Remove references the `doc' Makefile from the RPM build
specification. Instead, pass --install-man and --install-html to the
distutils build. (The latter differs from the Mercurial default by
adding the version. Note that the build specification hasn't been
tested thoroughly.)
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ PREFIX=/usr/local
export PREFIX
PYTHON=python
PURE=
-PYTHON_FILES:=$(shell find mercurial hgext doc -name '*.py')
+PYTHON_FILES:=$(shell find mercurial hgext -name '*.py')
help:
@echo 'Commonly used make targets:'
@@ -23,52 +23,39 @@ help:
@echo 'Example for a local installation (usable in this directory):'
@echo ' make local && ./hg version'
-all: build doc
+all: build
local:
- $(PYTHON) setup.py $(PURE) build_py -c -d . build_ext -i build_mo
+ $(PYTHON) setup.py $(PURE) --doc-format '' \
+ build_py -c -d . build_ext -i build_mo
$(PYTHON) hg version
build:
$(PYTHON) setup.py $(PURE) build
-doc:
- $(MAKE) -C doc
-
clean:
-$(PYTHON) setup.py clean --all # ignore errors from this command
find . -name '*.py[cdo]' -exec rm -f '{}' ';'
rm -f MANIFEST mercurial/__version__.py mercurial/*.so tests/*.err
rm -rf locale
- $(MAKE) -C doc clean
-install: install-bin install-doc
+install: install-bin
install-bin: build
$(PYTHON) setup.py $(PURE) install --prefix="$(PREFIX)" --force
-install-doc: doc
- cd doc && $(MAKE) $(MFLAGS) install
-
-install-home: install-home-bin install-home-doc
+install-home: install-home-bin
install-home-bin: build
$(PYTHON) setup.py $(PURE) install --home="$(HOME)" --force
-install-home-doc: doc
- cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
-
-MANIFEST-doc:
- $(MAKE) -C doc MANIFEST
-
-MANIFEST: MANIFEST-doc
+MANIFEST:
hg manifest > MANIFEST
echo mercurial/__version__.py >> MANIFEST
- cat doc/MANIFEST >> MANIFEST
dist: tests dist-notests
-dist-notests: doc MANIFEST
+dist-notests: MANIFEST
TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
tests:
@@ -98,6 +85,6 @@ i18n/hg.pot: $(PYTHON_FILES) help/*.txt
%.po: i18n/hg.pot
msgmerge --no-location --update $@ $^
-.PHONY: help all local build doc clean install install-bin install-doc \
- install-home install-home-bin install-home-doc dist dist-notests tests \
+.PHONY: help all local build clean install install-bin \
+ install-home install-home-bin dist dist-notests tests \
update-pot
diff --git a/contrib/mercurial.spec b/contrib/mercurial.spec
--- a/contrib/mercurial.spec
+++ b/contrib/mercurial.spec
@@ -35,8 +35,9 @@ make all
%install
rm -rf $RPM_BUILD_ROOT
-python setup.py install --root $RPM_BUILD_ROOT --prefix %{_prefix}
-make install-doc DESTDIR=$RPM_BUILD_ROOT MANDIR=%{_mandir}
+python setup.py install --root $RPM_BUILD_ROOT --prefix %{_prefix} \
+ --install-html '$install_data/doc/$dist_name-$dist_version' \
+ --install-man %{_mandir}
install contrib/hgk $RPM_BUILD_ROOT%{_bindir}
install contrib/convert-repo $RPM_BUILD_ROOT%{_bindir}/mercurial-convert-repo
diff --git a/doc/Makefile b/doc/Makefile
deleted file mode 100644
--- a/doc/Makefile
+++ /dev/null
@@ -1,49 +0,0 @@
-SOURCES=$(wildcard *.[0-9].txt)
-MAN=$(SOURCES:%.txt=%)
-HTML=$(SOURCES:%.txt=%.html)
-GENDOC=gendoc.py ../mercurial/commands.py ../mercurial/help.py ../help/*.txt
-PREFIX=/usr/local
-MANDIR=$(PREFIX)/share/man
-INSTALL=install -c -m 644
-PYTHON=python
-RST2HTML=$(shell which rst2html 2> /dev/null || which rst2html.py)
-
-export LC_ALL=C
-
-all: man html
-
-man: $(MAN)
-
-html: $(HTML)
-
-hg.1.txt: hg.1.gendoc.txt
- touch hg.1.txt
-
-hg.1.gendoc.txt: $(GENDOC)
- ${PYTHON} gendoc.py > $@.tmp
- mv $@.tmp $@
-
-%: %.txt common.txt
- $(PYTHON) rst2man.py --halt warning \
- --strip-elements-with-class htmlonly $*.txt $*
-
-%.html: %.txt common.txt
- $(RST2HTML) --halt warning \
- --link-stylesheet --stylesheet-path style.css $*.txt $*.html
-
-MANIFEST: man html
-# tracked files are already in the main MANIFEST
- $(RM) $@
- for i in $(MAN) $(HTML) hg.1.gendoc.txt; do \
- echo "doc/$$i" >> $@ ; \
- done
-
-install: man
- for i in $(MAN) ; do \
- subdir=`echo $$i | sed -n 's/^.*\.\([0-9]\)$$/man\1/p'` ; \
- mkdir -p $(DESTDIR)$(MANDIR)/$$subdir ; \
- $(INSTALL) $$i $(DESTDIR)$(MANDIR)/$$subdir ; \
- done
-
-clean:
- $(RM) $(MAN) $(MAN:%=%.html) *.[0-9].gendoc.txt MANIFEST
diff --git a/doc/__init__.py b/doc/__init__.py
new file mode 100644
diff --git a/doc/rst2man.py b/doc/manpage.py
rename from doc/rst2man.py
rename to doc/manpage.py
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -27,6 +27,7 @@ except:
"Couldn't import standard zlib (incomplete Python install).")
import os, subprocess, time
+import fnmatch
import shutil
import tempfile
@@ -52,6 +53,9 @@ scripts = ['hg']
if os.name == 'nt':
scripts.append('contrib/win32/hg.bat')
+# required for generated sources
+os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
+
# simplified version of distutils.ccompiler.CCompiler.has_function
# that actually removes its temporary files.
def has_function(cc, funcname):
@@ -245,10 +249,156 @@ class build_mo(build):
build.sub_commands.append(('build_mo', None))
+class build_doc(build):
+
+ description = "build documentation from ReST sources"
+
+ def extract_documentation(self, output):
+ '''
+ Extract documentation from the Mercurial sources.
+
+ We don't know the exact dependancies of the extraction. In order to
+ avoid regenerating *all* documentation on every run, we extract the
+ documentation, but only write to the output file when needed.
+ '''
+ from doc import gendoc
+ from cStringIO import StringIO
+
+ s = StringIO()
+ gendoc.show_doc(s)
+ s = s.getvalue()
+
+ if not (os.path.exists(output) and file(output).read() == s):
+ file(output, 'w').write(s)
+
+ def run(self):
+ '''
+ Build the Mercurial documentation from its ReST sources, including
+ extracting generated documentation using the `extract_documentation'
+ method.
+
+ This routine is somewhat complex, and can --- to a certain extent --- be
+ said to duplicate the functionality of docutils'
+ `publish_programatically' APIs. There are two reasons for this
+ complexity:
+
+ 1) We avoid parsing the documentation twice.
+ 2) We use the docutils API to always parse the documentation files and
+ extract their dependencies. This allows us to avoid generating
+ documentation when it is up-to-date w.r.t. its dependencies.
+
+ To put it in another way: For each source file, we use docutils to read
+ it and obtain its dependencies. Then, both manual pages and HTML
+ documentation is generated, but only when needed.
+ '''
+
+ doc_formats = set(self.distribution.doc_format.split(','))
+ if not doc_formats:
+ return
+
+ from os.path import join
+
+ try:
+ # import early, so we can fail early
+ from docutils import frontend, io, readers, writers
+ except ImportError:
+ self.warn('skipping documentation; docutils not found.')
+ return
+
+ # load & use the bundled rst2man
+ from doc import manpage
+
+ if not os.path.isdir('doc'):
+ self.warn("could not find doc directory")
+ return
+
+ # update extracted documentation
+ self.execute(self.extract_documentation,
+ [join('doc', 'hg.1.gendoc.txt')],
+ ('extracting %s from source files'
+ % join('doc', 'hg.1.gendoc.txt')))
+
+ datafiles = self.distribution.data_files
+
+ if 'html' in doc_formats:
+ datafiles.append(('$install_html', [join('doc', 'style.css')]))
+
+ for srcfile in fnmatch.filter(os.listdir('doc'),
+ "*.[1-9].txt"):
+ base = os.path.splitext(srcfile)[0]
+ name, category = base.rsplit('.', 1)
+
+ srcfile = join('doc', srcfile)
+ # manual file name is e.g. XXX.N
+ manfile = join('doc', base)
+ # HTML file name is e.g. XXX.N.html
+ htmlfile = join('doc', base + '.html')
+
+ # reader & writer classes
+ # NOTE: writers.get_writer_class('manpage') may be used eventually
+ reader = readers.get_reader_class('standalone')(parser_name='rst')
+ htmlwriter = writers.get_writer_class('html')()
+ manwriter = manpage.Writer()
+ components = (reader, reader.parser, htmlwriter, manwriter,)
+
+ # set shared docutils settings
+ optparser = frontend.OptionParser(components)
+ settings = optparser.parse_args(['--halt', 'warning',
+ '--link-stylesheet',
+ '--stylesheet-path', 'style.css'
+ '--option-limit' '0'])
+
+ # read & parse ReST source
+ reader.read(io.FileInput(source_path=srcfile, encoding='ascii'),
+ None, settings)
+
+ document = reader.document
+ document.transformer.populate_from_components(components)
+ document.transformer.apply_transforms()
+
+ # extract dependencies from docutils
+ dependencies = [srcfile] + settings.record_dependencies.list
+
+ # generate the documentation files; using distutils to avoid
+ # unnecessary processing
+ if 'html' in doc_formats:
+ self.make_file(infiles=dependencies, outfile=htmlfile,
+ func=htmlwriter.write,
+ args=(reader.document,
+ io.FileOutput(destination_path=htmlfile,
+ encoding='utf-8')),
+ exec_msg='generating %s' % htmlfile)
+ # add resulting files to distrubution
+ datafiles.append(('$install_html', [htmlfile]))
+
+ # set docutils settings specific to the manual writer
+ settings = optparser.parse_args(['--strip-elements-with-class',
+ 'htmlonly'], settings)
+
+ if 'man' in doc_formats:
+ self.make_file(infiles=dependencies, outfile=manfile,
+ func=manwriter.write,
+ args=(reader.document,
+ io.FileOutput(destination_path=manfile,
+ encoding='ascii')),
+ exec_msg='generating %s' % manfile)
+ datafiles.append((join('$install_man', 'man' + category),
+ [manfile]))
+
+
+build.sub_commands.append(('build_doc', None))
+
Distribution.pure = 0
Distribution.global_options.append(('pure', None, "use pure (slow) Python "
"code instead of C extensions"))
+if os.name == 'posix':
+ Distribution.doc_format = 'man,html'
+else:
+ Distribution.doc_format = 'html'
+Distribution.global_options.append(('doc-format=', None,
+ "documentation formats to generate"),)
+
class hg_build_py(build_py):
def finalize_options(self):
@@ -273,6 +423,7 @@ class hg_build_py(build_py):
cmdclass = {'install_data': install_extra_data,
'build_mo': build_mo,
+ 'build_doc': build_doc,
'build_py': hg_build_py}
ext_modules=[
More information about the Mercurial-devel
mailing list