[PATCH 5 of 6] setup: generate, build & install documentation using distutils & docutils

Dan Villiom Podlaski Christiansen danchr at gmail.com
Thu Nov 26 13:48:06 CST 2009


# HG changeset patch
# User Dan Villiom Podlaski Christiansen <danchr at gmail.com>
# Date 1259264725 -3600
# Node ID 912874aa86b1bfc1c273d7819a69fea75f9c8dff
# Parent  27a452aa1d0a9695b7d05a6e9601ac27626c4514
setup: generate, build & install documentation using distutils & docutils.

- Add a new dedicated build stage 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.

- Turn the `doc' directory into a package by adding an empty
  __init__.py file.

- Remove the Makefile in `doc', as it is now redundant, and 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-doc to the
  distutils build. (The latter differs from the Mercurial
  default by adding the version.) Please note that the build
  specification remains untested.

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,7 +23,7 @@ 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
@@ -32,43 +32,29 @@ local:
 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 +84,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-doc $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)
-
-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 \
-	  $< $@
-
-%.html: %.txt common.txt
-	$(RST2HTML) --halt warning \
-	  --option-limit 0 --link-stylesheet --stylesheet-path style.css \
-	  $< $@
-
-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):
@@ -259,6 +263,142 @@ 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.
+        '''
+
+        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
+
+        stylesheet = 'style.css'
+        datafiles.append((join('doc', self.distribution.get_name()),
+                          [join('doc', stylesheet)]))
+
+        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.html
+            htmlfile = join('doc', name + '.html')
+
+            mandestdir = join('$install_man', 'man' + category)
+            htmldestdir = '$install_doc'
+
+            # 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
+            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((htmldestdir, [htmlfile]))
+
+           # set docutils settings specific to the manual writer
+            settings = optparser.parse_args(['--strip-elements-with-class',
+                                             'htmlonly'], settings)
+
+            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((mandestdir, [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"))
@@ -287,6 +427,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