diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ff885997d95fa3f6f5e0f9e9b740e33f6a692bae..93fbea7790c864cf7083c12929f42e63a94c9d87 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -143,7 +143,6 @@ docs:
   - pip3 install sphinx-click
   - pip3 install sphinx_rtd_theme
   - cd dist && ./unpack.sh && cd buildstream
-  - pip3 install .
   - make BST_FORCE_SESSION_REBUILD=1 -C doc
   - cd ../..
   - mv dist/buildstream/doc/build/html public
diff --git a/doc/Makefile b/doc/Makefile
index 9a5e98754a1366bcf8be8639ed2827d39f151f88..66d2f0d2119bb42dfa0f0e8ad264a1e1891b759d 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -31,6 +31,9 @@ ifneq ($(strip $(BST_FORCE_SESSION_REBUILD)),)
 BST2HTMLOPTS = --force
 endif
 
+# Help Python find buildstream and its plugins
+PYTHONPATH=$(CURDIR)/..:$(CURDIR)/../buildstream/plugins
+
 
 .PHONY: all clean templates templates-clean sessions sessions-prep sessions-clean html devhelp
 
@@ -65,7 +68,6 @@ define plugin-doc-skeleton
 endef
 
 
-# We set PYTHONPATH here because source/conf.py sys.modules hacks dont seem to help sphinx-build import the plugins
 all: html devhelp
 
 clean: templates-clean sessions-clean
@@ -103,7 +105,7 @@ sessions-prep:
 #
 sessions: sessions-prep
 	for file in $(wildcard sessions/*.run); do		\
-	    $(BST2HTML) $(BST2HTMLOPTS) --description $$file;	\
+	    PYTHONPATH=$(PYTHONPATH) $(BST2HTML) $(BST2HTMLOPTS) --description $$file;	\
 	done
 
 sessions-clean:
@@ -114,7 +116,7 @@ sessions-clean:
 #
 html devhelp: templates sessions
 	@echo "Building $@..."
-	PYTHONPATH=$(CURDIR)/../buildstream/plugins \
+	PYTHONPATH=$(PYTHONPATH) \
 	    $(SPHINXBUILD) -b $@ $(ALLSPHINXOPTS) "$(BUILDDIR)/$@" \
 	    $(wildcard source/*.rst) \
 	    $(wildcard source/tutorial/*.rst) \
diff --git a/doc/bst2html.py b/doc/bst2html.py
index c9e98bb36746ec5c2e150d63352a6c3f8e784d94..d87085f544105fadc39e365de173bbadd81b3135 100755
--- a/doc/bst2html.py
+++ b/doc/bst2html.py
@@ -37,6 +37,7 @@ import click
 
 from buildstream import _yaml
 from buildstream import utils
+from buildstream._frontend import cli as bst_cli
 from buildstream._exceptions import BstError
 
 
@@ -175,6 +176,20 @@ def ansi2html(text, palette='solarized'):
     return sub
 
 
+@contextmanager
+def capture_stdout_stderr():
+    from io import StringIO
+
+    capture = StringIO()
+    oldstdout, sys.stdout = sys.stdout, capture
+    oldstderr, sys.stderr = sys.stderr, capture
+
+    yield capture
+
+    sys.stdout = oldstdout
+    sys.stderr = oldstderr
+
+
 # workdir()
 #
 # Sets up a new temp directory with a config file
@@ -219,10 +234,15 @@ def workdir(source_cache=None):
 def run_command(config_file, directory, command):
     click.echo("Running command in directory '{}': bst {}".format(directory, command), err=True)
 
-    argv = ['bst', '--colors', '--config', config_file] + shlex.split(command)
-    p = subprocess.Popen(argv, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-    out, _ = p.communicate()
-    return out.decode('utf-8').strip()
+    args = ['--colors', '--config', config_file, '--directory', directory] + shlex.split(command)
+
+    with capture_stdout_stderr() as capture:
+        bst_cli.main(args=args, prog_name=bst_cli.name)
+
+        capture.flush()
+        out = capture.getvalue()
+
+    return out.strip()
 
 
 # generate_html