blob: f0a09f1687f44ea9de590ceefc4c321275da4a26 [file] [log] [blame]
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +00001# Copyright (C) 2011 Igalia S.L.
mrobinson@webkit.org59096b22011-11-10 03:00:43 +00002#
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +00003# This library is free software; you can redistribute it and/or
4# modify it under the terms of the GNU Lesser General Public
5# License as published by the Free Software Foundation; either
6# version 2 of the License, or (at your option) any later version.
mrobinson@webkit.org59096b22011-11-10 03:00:43 +00007#
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +00008# This library is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11# Lesser General Public License for more details.
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000012#
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +000013# You should have received a copy of the GNU Lesser General Public
14# License along with this library; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000016
carlosgc@webkit.org38ec27b2018-04-20 10:33:30 +000017import codecs
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000018import errno
19import logging
20import os
21import os.path
22import subprocess
23import sys
24
25
26class GTKDoc(object):
27
28 """Class that controls a gtkdoc run.
29
30 Each instance of this class represents one gtkdoc configuration
31 and set of documentation. The gtkdoc package is a series of tools
32 run consecutively which converts inline C/C++ documentation into
33 docbook files and then into HTML. This class is suitable for
34 generating documentation or simply verifying correctness.
35
36 Keyword arguments:
37 output_dir -- The path where gtkdoc output should be placed. Generation
38 may overwrite file in this directory. Required.
39 module_name -- The name of the documentation module. For libraries this
40 is typically the library name. Required if not library path
41 is given.
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +000042 source_dirs -- A list of paths to directories of source code to be scanned.
43 Required if headers is not specified.
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000044 ignored_files -- A list of filenames to ignore in the source directory. It is
45 only necessary to provide the basenames of these files.
46 Typically it is important to provide an updated list of
47 ignored files to prevent warnings about undocumented symbols.
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +000048 headers -- A list of paths to headers to be scanned. Required if source_dirs
49 is not specified.
carlosgc@webkit.orgf4dfe8e2013-11-08 08:12:09 +000050 namespace -- The library namespace.
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000051 decorator -- If a decorator is used to unhide certain symbols in header
52 files this parameter is required for successful scanning.
53 (default '')
54 deprecation_guard -- gtkdoc tries to ensure that symbols marked as deprecated
55 are encased in this C preprocessor define. This is required
56 to avoid gtkdoc warnings. (default '')
57 cflags -- This parameter specifies any preprocessor flags necessary for
58 building the scanner binary during gtkdoc-scanobj. Typically
59 this includes all absolute include paths necessary to resolve
60 all header dependencies. (default '')
61 ldflags -- This parameter specifies any linker flags necessary for
62 building the scanner binary during gtkdoc-scanobj. Typically
63 this includes "-lyourlibraryname". (default '')
64 library_path -- This parameter specifies the path to the directory where you
65 library resides used for building the scanner binary during
66 gtkdoc-scanobj. (default '')
67
68 doc_dir -- The path to other documentation files necessary to build
69 the documentation. This files in this directory as well as
70 the files in the 'html' subdirectory will be copied
71 recursively into the output directory. (default '')
72 main_sgml_file -- The path or name (if a doc_dir is given) of the SGML file
73 that is the considered the main page of your documentation.
74 (default: <module_name>-docs.sgml)
75 version -- The version number of the module. If this is provided,
76 a version.xml file containing the version will be created
77 in the output directory during documentation generation.
78
79 interactive -- Whether or not errors or warnings should prompt the user
80 to continue or not. When this value is false, generation
81 will continue despite warnings. (default False)
kov@webkit.org316a9072012-02-09 02:28:31 +000082
83 virtual_root -- A temporary installation directory which is used as the root
84 where the actual installation prefix lives; this is mostly
85 useful for packagers, and should be set to what is given to
86 make install as DESTDIR.
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000087 """
88
carlosgc@webkit.org7bd55972012-02-01 18:04:57 +000089 def __init__(self, args):
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000090
91 # Parameters specific to scanning.
92 self.module_name = ''
93 self.source_dirs = []
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +000094 self.headers = []
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000095 self.ignored_files = []
carlosgc@webkit.orgf4dfe8e2013-11-08 08:12:09 +000096 self.namespace = ''
mrobinson@webkit.org59096b22011-11-10 03:00:43 +000097 self.decorator = ''
98 self.deprecation_guard = ''
99
100 # Parameters specific to gtkdoc-scanobj.
101 self.cflags = ''
102 self.ldflags = ''
103 self.library_path = ''
104
105 # Parameters specific to generation.
106 self.output_dir = ''
107 self.doc_dir = ''
108 self.main_sgml_file = ''
109
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000110 # Parameters specific to gtkdoc-fixxref.
111 self.cross_reference_deps = []
112
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000113 self.interactive = False
114
115 self.logger = logging.getLogger('gtkdoc')
116
zandobersek@gmail.comd23a5a12013-01-08 10:10:24 +0000117 for key, value in iter(args.items()):
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000118 setattr(self, key, value)
119
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +0000120 if not getattr(self, 'output_dir'):
121 raise Exception('output_dir not specified.')
122 if not getattr(self, 'module_name'):
123 raise Exception('module_name not specified.')
124 if not getattr(self, 'source_dirs') and not getattr(self, 'headers'):
125 raise Exception('Neither source_dirs nor headers specified.' % key)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000126
127 # Make all paths absolute in case we were passed relative paths, since
128 # we change the current working directory when executing subcommands.
129 self.output_dir = os.path.abspath(self.output_dir)
130 self.source_dirs = [os.path.abspath(x) for x in self.source_dirs]
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +0000131 self.headers = [os.path.abspath(x) for x in self.headers]
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000132 if self.library_path:
133 self.library_path = os.path.abspath(self.library_path)
134
135 if not self.main_sgml_file:
136 self.main_sgml_file = self.module_name + "-docs.sgml"
137
138 def generate(self, html=True):
139 self.saw_warnings = False
140
141 self._copy_doc_files_to_output_dir(html)
142 self._write_version_xml()
143 self._run_gtkdoc_scan()
144 self._run_gtkdoc_scangobj()
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000145 self._run_gtkdoc_mkdb()
146
147 if not html:
148 return
149
150 self._run_gtkdoc_mkhtml()
151 self._run_gtkdoc_fixxref()
152
153 def _delete_file_if_exists(self, path):
154 if not os.access(path, os.F_OK | os.R_OK):
155 return
156 self.logger.debug('deleting %s', path)
157 os.unlink(path)
158
159 def _create_directory_if_nonexistent(self, path):
160 try:
161 os.makedirs(path)
162 except OSError as error:
163 if error.errno != errno.EEXIST:
164 raise
165
166 def _raise_exception_if_file_inaccessible(self, path):
167 if not os.path.exists(path) or not os.access(path, os.R_OK):
168 raise Exception("Could not access file at: %s" % path)
169
170 def _output_has_warnings(self, outputs):
171 for output in outputs:
172 if output and output.find('warning'):
173 return True
174 return False
175
176 def _ask_yes_or_no_question(self, question):
177 if not self.interactive:
178 return True
179
180 question += ' [y/N] '
181 answer = None
182 while answer != 'y' and answer != 'n' and answer != '':
183 answer = raw_input(question).lower()
184 return answer == 'y'
185
carlosgc@webkit.org01454612017-01-21 10:21:33 +0000186 def _run_command(self, args, env=None, cwd=None, print_output=True, ignore_warnings=False):
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +0000187 if print_output:
carlosgc@webkit.org01454612017-01-21 10:21:33 +0000188 self.logger.debug("Running %s", args[0])
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000189 self.logger.debug("Full command args: %s", str(args))
190
191 process = subprocess.Popen(args, env=env, cwd=cwd,
192 stdout=subprocess.PIPE,
193 stderr=subprocess.PIPE)
zandobersek@gmail.comd23a5a12013-01-08 10:10:24 +0000194 stdout, stderr = [b.decode("utf-8") for b in process.communicate()]
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000195
196 if print_output:
197 if stdout:
carlosgc@webkit.org7ebab882014-10-04 07:35:47 +0000198 try:
199 sys.stdout.write(stdout.encode("utf-8"))
200 except UnicodeDecodeError:
201 sys.stdout.write(stdout)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000202 if stderr:
carlosgc@webkit.org7ebab882014-10-04 07:35:47 +0000203 try:
204 sys.stderr.write(stderr.encode("utf-8"))
205 except UnicodeDecodeError:
206 sys.stderr.write(stderr)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000207
208 if process.returncode != 0:
209 raise Exception('%s produced a non-zero return code %i'
210 % (args[0], process.returncode))
211
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +0000212 if not ignore_warnings and ('warning' in stderr or 'warning' in stdout):
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000213 self.saw_warnings = True
214 if not self._ask_yes_or_no_question('%s produced warnings, '
215 'try to continue?' % args[0]):
216 raise Exception('%s step failed' % args[0])
217
218 return stdout.strip()
219
220 def _copy_doc_files_to_output_dir(self, html=True):
221 if not self.doc_dir:
222 self.logger.info('Not copying any files from doc directory,'
223 ' because no doc directory given.')
224 return
225
226 def copy_file_replacing_existing(src, dest):
227 if os.path.isdir(src):
228 self.logger.debug('skipped directory %s', src)
229 return
230 if not os.access(src, os.F_OK | os.R_OK):
231 self.logger.debug('skipped unreadable %s', src)
232 return
233
234 self._delete_file_if_exists(dest)
235
236 self.logger.debug('created %s', dest)
berto@igalia.comac25f332013-09-14 18:03:40 +0000237 try:
238 os.link(src, dest)
239 except OSError:
240 os.symlink(src, dest)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000241
242 def copy_all_files_in_directory(src, dest):
243 for path in os.listdir(src):
244 copy_file_replacing_existing(os.path.join(src, path),
245 os.path.join(dest, path))
246
mcatanzaro@igalia.com0389b672016-12-19 13:02:54 +0000247 self.logger.debug('Copying template files to output directory...')
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000248 self._create_directory_if_nonexistent(self.output_dir)
249 copy_all_files_in_directory(self.doc_dir, self.output_dir)
250
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000251 def _write_version_xml(self):
252 if not self.version:
253 self.logger.info('No version specified, so not writing version.xml')
254 return
255
256 version_xml_path = os.path.join(self.output_dir, 'version.xml')
257 src_version_xml_path = os.path.join(self.doc_dir, 'version.xml')
258
259 # Don't overwrite version.xml if it was in the doc directory.
260 if os.path.exists(version_xml_path) and \
261 os.path.exists(src_version_xml_path):
262 return
263
264 output_file = open(version_xml_path, 'w')
265 output_file.write(self.version)
266 output_file.close()
267
carlosgc@webkit.org76c235d2012-02-06 09:29:34 +0000268 def _ignored_files_basenames(self):
269 return ' '.join([os.path.basename(x) for x in self.ignored_files])
270
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000271 def _run_gtkdoc_scan(self):
272 args = ['gtkdoc-scan',
273 '--module=%s' % self.module_name,
274 '--rebuild-types']
275
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +0000276 if not self.headers:
277 # Each source directory should be have its own "--source-dir=" prefix.
278 args.extend(['--source-dir=%s' % path for path in self.source_dirs])
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000279
280 if self.decorator:
281 args.append('--ignore-decorators=%s' % self.decorator)
282 if self.deprecation_guard:
283 args.append('--deprecated-guards=%s' % self.deprecation_guard)
284 if self.output_dir:
285 args.append('--output-dir=%s' % self.output_dir)
286
mrobinson@webkit.org231f2a02014-02-25 07:30:21 +0000287 # We only need to pass the list of ignored files if the we are not using an explicit list of headers.
288 if not self.headers:
289 # gtkdoc-scan wants the basenames of ignored headers, so strip the
290 # dirname. Different from "--source-dir", the headers should be
291 # specified as one long string.
292 ignored_files_basenames = self._ignored_files_basenames()
293 if ignored_files_basenames:
294 args.append('--ignore-headers=%s' % ignored_files_basenames)
295
296 if self.headers:
297 args.extend(self.headers)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000298
299 self._run_command(args)
300
301 def _run_gtkdoc_scangobj(self):
302 env = os.environ
303 ldflags = self.ldflags
304 if self.library_path:
commit-queue@webkit.org77ff9502014-11-17 10:48:39 +0000305 additional_ldflags = ''
306 for arg in env.get('LDFLAGS', '').split(' '):
307 if arg.startswith('-L'):
308 additional_ldflags = '%s %s' % (additional_ldflags, arg)
309 ldflags = ' "-L%s" %s ' % (self.library_path, additional_ldflags) + ldflags
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +0000310 current_ld_library_path = env.get('LD_LIBRARY_PATH')
311 if current_ld_library_path:
mcatanzaro@igalia.combdb85f42017-11-04 16:47:46 +0000312 env['LD_LIBRARY_PATH'] = '%s:%s' % (self.library_path, current_ld_library_path)
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +0000313 else:
mcatanzaro@igalia.combdb85f42017-11-04 16:47:46 +0000314 env['LD_LIBRARY_PATH'] = self.library_path
mrobinson@webkit.org150f9cf2011-11-25 16:19:44 +0000315
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000316 if ldflags:
carlosgc@webkit.org94370612012-01-16 12:19:41 +0000317 env['LDFLAGS'] = '%s %s' % (ldflags, env.get('LDFLAGS', ''))
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000318 if self.cflags:
carlosgc@webkit.org94370612012-01-16 12:19:41 +0000319 env['CFLAGS'] = '%s %s' % (self.cflags, env.get('CFLAGS', ''))
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000320
321 if 'CFLAGS' in env:
322 self.logger.debug('CFLAGS=%s', env['CFLAGS'])
323 if 'LDFLAGS' in env:
324 self.logger.debug('LDFLAGS %s', env['LDFLAGS'])
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000325 self._run_command(['gtkdoc-scangobj', '--module=%s' % self.module_name],
326 env=env, cwd=self.output_dir)
327
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000328 def _run_gtkdoc_mkdb(self):
329 sgml_file = os.path.join(self.output_dir, self.main_sgml_file)
330 self._raise_exception_if_file_inaccessible(sgml_file)
331
332 args = ['gtkdoc-mkdb',
333 '--module=%s' % self.module_name,
334 '--main-sgml-file=%s' % sgml_file,
335 '--source-suffixes=h,c,cpp,cc',
336 '--output-format=xml',
337 '--sgml-mode']
338
carlosgc@webkit.orgf4dfe8e2013-11-08 08:12:09 +0000339 if self.namespace:
340 args.append('--name-space=%s' % self.namespace)
341
carlosgc@webkit.org76c235d2012-02-06 09:29:34 +0000342 ignored_files_basenames = self._ignored_files_basenames()
343 if ignored_files_basenames:
344 args.append('--ignore-files=%s' % ignored_files_basenames)
345
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000346 # Each directory should be have its own "--source-dir=" prefix.
347 args.extend(['--source-dir=%s' % path for path in self.source_dirs])
348 self._run_command(args, cwd=self.output_dir)
349
350 def _run_gtkdoc_mkhtml(self):
carlosgc@webkit.org38ec27b2018-04-20 10:33:30 +0000351 # gtkdoc-fixxref expects the paths to be html/modulename.
352 html_dest_dir = os.path.join(self.output_dir, 'html', self.module_name)
353 self._create_directory_if_nonexistent(html_dest_dir)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000354 if not os.path.isdir(html_dest_dir):
355 raise Exception("%s is not a directory, could not generate HTML"
356 % html_dest_dir)
357 elif not os.access(html_dest_dir, os.X_OK | os.R_OK | os.W_OK):
358 raise Exception("Could not access %s to generate HTML"
359 % html_dest_dir)
360
361 # gtkdoc-mkhtml expects the SGML path to be absolute.
362 sgml_file = os.path.join(os.path.abspath(self.output_dir),
363 self.main_sgml_file)
364 self._raise_exception_if_file_inaccessible(sgml_file)
365
366 self._run_command(['gtkdoc-mkhtml', self.module_name, sgml_file],
367 cwd=html_dest_dir)
368
369 def _run_gtkdoc_fixxref(self):
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000370 args = ['gtkdoc-fixxref',
commit-queue@webkit.org4231b202017-05-20 19:56:40 +0000371 '--module=%s' % self.module_name,
carlosgc@webkit.org38ec27b2018-04-20 10:33:30 +0000372 '--module-dir=html/%s' % self.module_name]
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000373 args.extend(['--extra-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps])
374 self._run_command(args, cwd=self.output_dir, ignore_warnings=True)
375
carlosgc@webkit.org38ec27b2018-04-20 10:33:30 +0000376 # gtkdoc-fixxref has some predefined links for which it always uses absolute paths.
377 html_dir_prefix = os.path.join(self.virtual_root + self.prefix, 'share', 'gtk-doc', 'html')
378 module_dir = os.path.join(self.output_dir, 'html', self.module_name)
379 for entry in os.listdir(module_dir):
380 if not entry.endswith('.html'):
381 continue
382
383 filename = os.path.join(module_dir, entry)
384 contents = ''
385 with codecs.open(filename, 'r', encoding='utf-8') as f:
386 contents = f.read()
387
388 if not html_dir_prefix in contents:
389 continue
390
391 tmp_filename = filename + '.new'
392 new_contents = contents.replace(html_dir_prefix, '..')
393 with codecs.open(tmp_filename, 'w', encoding='utf-8') as f:
394 f.write(new_contents)
395
396 os.rename(tmp_filename, filename)
397
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000398 def rebase_installed_docs(self):
commit-queue@webkit.org0b338472012-09-04 18:33:10 +0000399 if not os.path.isdir(self.output_dir):
400 raise Exception("Tried to rebase documentation before generating it.")
kov@webkit.org316a9072012-02-09 02:28:31 +0000401 html_dir = os.path.join(self.virtual_root + self.prefix, 'share', 'gtk-doc', 'html', self.module_name)
carlosgc@webkit.org0313edc2012-02-06 14:54:38 +0000402 if not os.path.isdir(html_dir):
403 return
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000404 args = ['gtkdoc-rebase',
405 '--relative',
406 '--html-dir=%s' % html_dir]
407 args.extend(['--other-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps])
kov@webkit.org316a9072012-02-09 02:28:31 +0000408 if self.virtual_root:
409 args.extend(['--dest-dir=%s' % self.virtual_root])
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000410 self._run_command(args, cwd=self.output_dir)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000411
commit-queue@webkit.org0b338472012-09-04 18:33:10 +0000412 def api_missing_documentation(self):
413 unused_doc_file = os.path.join(self.output_dir, self.module_name + "-unused.txt")
414 if not os.path.exists(unused_doc_file) or not os.access(unused_doc_file, os.R_OK):
415 return []
416 return open(unused_doc_file).read().splitlines()
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000417
418class PkgConfigGTKDoc(GTKDoc):
419
420 """Class reads a library's pkgconfig file to guess gtkdoc parameters.
421
422 Some gtkdoc parameters can be guessed by reading a library's pkgconfig
423 file, including the cflags, ldflags and version parameters. If you
424 provide these parameters as well, they will be appended to the ones
425 guessed via the pkgconfig file.
426
427 Keyword arguments:
428 pkg_config_path -- Path to the pkgconfig file for the library. Required.
429 """
430
carlosgc@webkit.org7bd55972012-02-01 18:04:57 +0000431 def __init__(self, pkg_config_path, args):
432 super(PkgConfigGTKDoc, self).__init__(args)
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000433
commit-queue@webkit.org5c6df642013-06-03 22:25:19 +0000434 pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config')
435
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000436 if not os.path.exists(pkg_config_path):
437 raise Exception('Could not find pkg-config file at: %s'
438 % pkg_config_path)
439
commit-queue@webkit.org5c6df642013-06-03 22:25:19 +0000440 self.cflags += " " + self._run_command([pkg_config,
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000441 pkg_config_path,
442 '--cflags'], print_output=False)
commit-queue@webkit.org5c6df642013-06-03 22:25:19 +0000443 self.ldflags += " " + self._run_command([pkg_config,
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000444 pkg_config_path,
445 '--libs'], print_output=False)
commit-queue@webkit.org5c6df642013-06-03 22:25:19 +0000446 self.version = self._run_command([pkg_config,
mrobinson@webkit.org59096b22011-11-10 03:00:43 +0000447 pkg_config_path,
448 '--modversion'], print_output=False)
commit-queue@webkit.org5c6df642013-06-03 22:25:19 +0000449 self.prefix = self._run_command([pkg_config,
carlosgc@webkit.orga5df5392012-02-06 09:23:08 +0000450 pkg_config_path,
451 '--variable=prefix'], print_output=False)