Tristan Van Berkom pushed to branch Qinusty/563-cache-quota-restriction at BuildStream / buildstream
Commits:
-
98616f50
by Qinusty at 2018-08-03T16:30:24Z
-
111c0fde
by Mathieu Bridon at 2018-08-05T09:40:22Z
-
d9b2de3e
by Tristan Van Berkom at 2018-08-05T10:06:42Z
-
10d57730
by Tristan Van Berkom at 2018-08-05T10:06:42Z
-
574fb6ac
by Tristan Van Berkom at 2018-08-05T10:06:42Z
-
81fdd088
by Tristan Van Berkom at 2018-08-05T10:59:38Z
-
16221c6b
by Josh Smith at 2018-08-06T09:49:13Z
7 changed files:
- .gitlab-ci.yml
- HACKING.rst
- + buildstream/__main__.py
- buildstream/_context.py
- buildstream/utils.py
- doc/Makefile
- doc/bst2html.py
Changes:
... | ... | @@ -143,7 +143,6 @@ docs: |
143 | 143 |
- pip3 install sphinx-click
|
144 | 144 |
- pip3 install sphinx_rtd_theme
|
145 | 145 |
- cd dist && ./unpack.sh && cd buildstream
|
146 |
- - pip3 install .
|
|
147 | 146 |
- make BST_FORCE_SESSION_REBUILD=1 -C doc
|
148 | 147 |
- cd ../..
|
149 | 148 |
- mv dist/buildstream/doc/build/html public
|
... | ... | @@ -256,9 +256,6 @@ using pip or some other mechanism:: |
256 | 256 |
# Additional optional dependencies required
|
257 | 257 |
pip3 install --user arpy
|
258 | 258 |
|
259 |
-Furthermore, the documentation build requires that BuildStream itself
|
|
260 |
-be installed, as it will be used in the process of generating its docs.
|
|
261 |
- |
|
262 | 259 |
To build the documentation, just run the following::
|
263 | 260 |
|
264 | 261 |
make -C doc
|
1 |
+##################################################################
|
|
2 |
+# Private Entry Point #
|
|
3 |
+##################################################################
|
|
4 |
+#
|
|
5 |
+# This allows running the cli when BuildStream is uninstalled,
|
|
6 |
+# as long as BuildStream repo is in PYTHONPATH, one can run it
|
|
7 |
+# with:
|
|
8 |
+#
|
|
9 |
+# python3 -m buildstream [program args]
|
|
10 |
+#
|
|
11 |
+# This is used when we need to run BuildStream before installing,
|
|
12 |
+# like when we build documentation.
|
|
13 |
+#
|
|
14 |
+if __name__ == '__main__':
|
|
15 |
+ # pylint: disable=no-value-for-parameter
|
|
16 |
+ from ._frontend.cli import cli
|
|
17 |
+ cli()
|
... | ... | @@ -197,29 +197,55 @@ class Context(): |
197 | 197 |
"\nValid values are, for example: 800M 10G 1T 50%\n"
|
198 | 198 |
.format(str(e))) from e
|
199 | 199 |
|
200 |
- # If we are asked not to set a quota, we set it to the maximum
|
|
201 |
- # disk space available minus a headroom of 2GB, such that we
|
|
202 |
- # at least try to avoid raising Exceptions.
|
|
200 |
+ # Headroom intended to give BuildStream a bit of leeway.
|
|
201 |
+ # This acts as the minimum size of cache_quota and also
|
|
202 |
+ # is taken from the user requested cache_quota.
|
|
203 | 203 |
#
|
204 |
- # Of course, we might still end up running out during a build
|
|
205 |
- # if we end up writing more than 2G, but hey, this stuff is
|
|
206 |
- # already really fuzzy.
|
|
207 |
- #
|
|
208 |
- if cache_quota is None:
|
|
209 |
- stat = os.statvfs(artifactdir_volume)
|
|
210 |
- # Again, the artifact directory may not yet have been
|
|
211 |
- # created
|
|
212 |
- if not os.path.exists(self.artifactdir):
|
|
213 |
- cache_size = 0
|
|
214 |
- else:
|
|
215 |
- cache_size = utils._get_dir_size(self.artifactdir)
|
|
216 |
- cache_quota = cache_size + stat.f_bsize * stat.f_bavail
|
|
217 |
- |
|
218 | 204 |
if 'BST_TEST_SUITE' in os.environ:
|
219 | 205 |
headroom = 0
|
220 | 206 |
else:
|
221 | 207 |
headroom = 2e9
|
222 | 208 |
|
209 |
+ stat = os.statvfs(artifactdir_volume)
|
|
210 |
+ available_space = (stat.f_bsize * stat.f_bavail)
|
|
211 |
+ |
|
212 |
+ # Again, the artifact directory may not yet have been created yet
|
|
213 |
+ #
|
|
214 |
+ if not os.path.exists(self.artifactdir):
|
|
215 |
+ cache_size = 0
|
|
216 |
+ else:
|
|
217 |
+ cache_size = utils._get_dir_size(self.artifactdir)
|
|
218 |
+ |
|
219 |
+ # Ensure system has enough storage for the cache_quota
|
|
220 |
+ #
|
|
221 |
+ # If cache_quota is none, set it to the maximum it could possibly be.
|
|
222 |
+ #
|
|
223 |
+ # Also check that cache_quota is atleast as large as our headroom.
|
|
224 |
+ #
|
|
225 |
+ if cache_quota is None: # Infinity, set to max system storage
|
|
226 |
+ cache_quota = cache_size + available_space
|
|
227 |
+ if cache_quota < headroom: # Check minimum
|
|
228 |
+ raise LoadError(LoadErrorReason.INVALID_DATA,
|
|
229 |
+ "Invalid cache quota ({}): ".format(utils._pretty_size(cache_quota)) +
|
|
230 |
+ "BuildStream requires a minimum cache quota of 2G.")
|
|
231 |
+ elif cache_quota > cache_size + available_space: # Check maximum
|
|
232 |
+ raise LoadError(LoadErrorReason.INVALID_DATA,
|
|
233 |
+ ("Your system does not have enough available " +
|
|
234 |
+ "space to support the cache quota specified.\n" +
|
|
235 |
+ "You currently have:\n" +
|
|
236 |
+ "- {used} of cache in use at {local_cache_path}\n" +
|
|
237 |
+ "- {available} of available system storage").format(
|
|
238 |
+ used=utils._pretty_size(cache_size),
|
|
239 |
+ local_cache_path=self.artifactdir,
|
|
240 |
+ available=utils._pretty_size(available_space)))
|
|
241 |
+ |
|
242 |
+ # Place a slight headroom (2e9 (2GB) on the cache_quota) into
|
|
243 |
+ # cache_quota to try and avoid exceptions.
|
|
244 |
+ #
|
|
245 |
+ # Of course, we might still end up running out during a build
|
|
246 |
+ # if we end up writing more than 2G, but hey, this stuff is
|
|
247 |
+ # already really fuzzy.
|
|
248 |
+ #
|
|
223 | 249 |
self.cache_quota = cache_quota - headroom
|
224 | 250 |
self.cache_lower_threshold = self.cache_quota / 2
|
225 | 251 |
|
... | ... | @@ -612,6 +612,27 @@ def _parse_size(size, volume): |
612 | 612 |
return int(num) * 1024**units.index(unit)
|
613 | 613 |
|
614 | 614 |
|
615 |
+# _pretty_size()
|
|
616 |
+#
|
|
617 |
+# Converts a number of bytes into a string representation in KB, MB, GB, TB
|
|
618 |
+# represented as K, M, G, T etc.
|
|
619 |
+#
|
|
620 |
+# Args:
|
|
621 |
+# size (int): The size to convert in bytes.
|
|
622 |
+# dec_places (int): The number of decimal places to output to.
|
|
623 |
+#
|
|
624 |
+# Returns:
|
|
625 |
+# (str): The string representation of the number of bytes in the largest
|
|
626 |
+def _pretty_size(size, dec_places=0):
|
|
627 |
+ psize = size
|
|
628 |
+ unit = 'B'
|
|
629 |
+ for unit in ('B', 'K', 'M', 'G', 'T'):
|
|
630 |
+ if psize < 1024:
|
|
631 |
+ break
|
|
632 |
+ else:
|
|
633 |
+ psize /= 1024
|
|
634 |
+ return "{size:g}{unit}".format(size=round(psize, dec_places), unit=unit)
|
|
635 |
+ |
|
615 | 636 |
# A sentinel to be used as a default argument for functions that need
|
616 | 637 |
# to distinguish between a kwarg set to None and an unset kwarg.
|
617 | 638 |
_sentinel = object()
|
... | ... | @@ -31,6 +31,9 @@ ifneq ($(strip $(BST_FORCE_SESSION_REBUILD)),) |
31 | 31 |
BST2HTMLOPTS = --force
|
32 | 32 |
endif
|
33 | 33 |
|
34 |
+# Help Python find buildstream and its plugins
|
|
35 |
+PYTHONPATH=$(CURDIR)/..:$(CURDIR)/../buildstream/plugins
|
|
36 |
+ |
|
34 | 37 |
|
35 | 38 |
.PHONY: all clean templates templates-clean sessions sessions-prep sessions-clean html devhelp
|
36 | 39 |
|
... | ... | @@ -65,7 +68,6 @@ define plugin-doc-skeleton |
65 | 68 |
endef
|
66 | 69 |
|
67 | 70 |
|
68 |
-# We set PYTHONPATH here because source/conf.py sys.modules hacks dont seem to help sphinx-build import the plugins
|
|
69 | 71 |
all: html devhelp
|
70 | 72 |
|
71 | 73 |
clean: templates-clean sessions-clean
|
... | ... | @@ -103,7 +105,7 @@ sessions-prep: |
103 | 105 |
#
|
104 | 106 |
sessions: sessions-prep
|
105 | 107 |
for file in $(wildcard sessions/*.run); do \
|
106 |
- $(BST2HTML) $(BST2HTMLOPTS) --description $$file; \
|
|
108 |
+ PYTHONPATH=$(PYTHONPATH) $(BST2HTML) $(BST2HTMLOPTS) $$file; \
|
|
107 | 109 |
done
|
108 | 110 |
|
109 | 111 |
sessions-clean:
|
... | ... | @@ -114,7 +116,7 @@ sessions-clean: |
114 | 116 |
#
|
115 | 117 |
html devhelp: templates sessions
|
116 | 118 |
@echo "Building $@..."
|
117 |
- PYTHONPATH=$(CURDIR)/../buildstream/plugins \
|
|
119 |
+ PYTHONPATH=$(PYTHONPATH) \
|
|
118 | 120 |
$(SPHINXBUILD) -b $@ $(ALLSPHINXOPTS) "$(BUILDDIR)/$@" \
|
119 | 121 |
$(wildcard source/*.rst) \
|
120 | 122 |
$(wildcard source/tutorial/*.rst) \
|
... | ... | @@ -204,7 +204,7 @@ def workdir(source_cache=None): |
204 | 204 |
yield (tempdir, bst_config_file, source_cache)
|
205 | 205 |
|
206 | 206 |
|
207 |
-# run_command()
|
|
207 |
+# run_bst_command()
|
|
208 | 208 |
#
|
209 | 209 |
# Runs a command
|
210 | 210 |
#
|
... | ... | @@ -216,10 +216,30 @@ def workdir(source_cache=None): |
216 | 216 |
# Returns:
|
217 | 217 |
# (str): The colorized combined stdout/stderr of BuildStream
|
218 | 218 |
#
|
219 |
-def run_command(config_file, directory, command):
|
|
220 |
- click.echo("Running command in directory '{}': bst {}".format(directory, command), err=True)
|
|
219 |
+def run_bst_command(config_file, directory, command):
|
|
220 |
+ click.echo("Running bst command in directory '{}': bst {}".format(directory, command), err=True)
|
|
221 | 221 |
|
222 |
- argv = ['bst', '--colors', '--config', config_file] + shlex.split(command)
|
|
222 |
+ argv = ['python3', '-m', 'buildstream', '--colors', '--config', config_file] + shlex.split(command)
|
|
223 |
+ p = subprocess.Popen(argv, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
224 |
+ out, _ = p.communicate()
|
|
225 |
+ return out.decode('utf-8').strip()
|
|
226 |
+ |
|
227 |
+ |
|
228 |
+# run_shell_command()
|
|
229 |
+#
|
|
230 |
+# Runs a command
|
|
231 |
+#
|
|
232 |
+# Args:
|
|
233 |
+# directory (str): The project directory
|
|
234 |
+# command (str): A shell command
|
|
235 |
+#
|
|
236 |
+# Returns:
|
|
237 |
+# (str): The combined stdout/stderr of the shell command
|
|
238 |
+#
|
|
239 |
+def run_shell_command(directory, command):
|
|
240 |
+ click.echo("Running shell command in directory '{}': {}".format(directory, command), err=True)
|
|
241 |
+ |
|
242 |
+ argv = shlex.split(command)
|
|
223 | 243 |
p = subprocess.Popen(argv, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
224 | 244 |
out, _ = p.communicate()
|
225 | 245 |
return out.decode('utf-8').strip()
|
... | ... | @@ -373,12 +393,18 @@ def run_session(description, tempdir, source_cache, palette, config_file, force) |
373 | 393 |
# Get the command string
|
374 | 394 |
command_str = _yaml.node_get(command, str, 'command')
|
375 | 395 |
|
396 |
+ # Check whether this is a shell command and not a bst command
|
|
397 |
+ is_shell = _yaml.node_get(command, bool, 'shell', default_value=False)
|
|
398 |
+ |
|
376 | 399 |
# Check if there is fake output
|
377 | 400 |
command_fake_output = _yaml.node_get(command, str, 'fake-output', default_value=None)
|
378 | 401 |
|
379 | 402 |
# Run the command, or just use the fake output
|
380 | 403 |
if command_fake_output is None:
|
381 |
- command_out = run_command(config_file, directory, command_str)
|
|
404 |
+ if is_shell:
|
|
405 |
+ command_out = run_shell_command(directory, command_str)
|
|
406 |
+ else:
|
|
407 |
+ command_out = run_bst_command(config_file, directory, command_str)
|
|
382 | 408 |
else:
|
383 | 409 |
command_out = command_fake_output
|
384 | 410 |
|
... | ... | @@ -414,14 +440,8 @@ def run_session(description, tempdir, source_cache, palette, config_file, force) |
414 | 440 |
@click.option('--palette', '-p', default='tango',
|
415 | 441 |
type=click.Choice(['solarized', 'solarized-xterm', 'tango', 'xterm', 'console']),
|
416 | 442 |
help="Selects a palette for the output style")
|
417 |
-@click.option('--output', '-o',
|
|
418 |
- type=click.Path(file_okay=True, dir_okay=False, writable=True),
|
|
419 |
- help="A file to store the output")
|
|
420 |
-@click.option('--description', '-d',
|
|
421 |
- type=click.Path(file_okay=True, dir_okay=False, readable=True),
|
|
422 |
- help="A file describing what to do")
|
|
423 |
-@click.argument('command', type=click.STRING, nargs=-1)
|
|
424 |
-def run_bst(directory, force, source_cache, description, palette, output, command):
|
|
443 |
+@click.argument('description', click.Path(file_okay=True, dir_okay=False, readable=True))
|
|
444 |
+def run_bst(directory, force, source_cache, description, palette):
|
|
425 | 445 |
"""Run a bst command and capture stdout/stderr in html
|
426 | 446 |
|
427 | 447 |
This command normally takes a description yaml file, see the HACKING
|
... | ... | @@ -430,45 +450,8 @@ def run_bst(directory, force, source_cache, description, palette, output, comman |
430 | 450 |
if not source_cache and os.environ.get('BST_SOURCE_CACHE'):
|
431 | 451 |
source_cache = os.environ['BST_SOURCE_CACHE']
|
432 | 452 |
|
433 |
- if output is not None and not check_needs_build(None, output, force=force):
|
|
434 |
- click.echo("No need to rebuild {}".format(output))
|
|
435 |
- return 0
|
|
436 |
- |
|
437 | 453 |
with workdir(source_cache=source_cache) as (tempdir, config_file, source_cache):
|
438 |
- |
|
439 |
- if description:
|
|
440 |
- run_session(description, tempdir, source_cache, palette, config_file, force)
|
|
441 |
- return 0
|
|
442 |
- |
|
443 |
- # Run a command specified on the CLI
|
|
444 |
- #
|
|
445 |
- if not directory:
|
|
446 |
- directory = os.getcwd()
|
|
447 |
- else:
|
|
448 |
- directory = os.path.abspath(directory)
|
|
449 |
- directory = os.path.realpath(directory)
|
|
450 |
- |
|
451 |
- if not command:
|
|
452 |
- command = []
|
|
453 |
- command_str = " ".join(command)
|
|
454 |
- |
|
455 |
- # Run the command
|
|
456 |
- #
|
|
457 |
- command_out = run_command(config_file, directory, command_str)
|
|
458 |
- |
|
459 |
- # Generate a nice html div for this output
|
|
460 |
- #
|
|
461 |
- converted = generate_html(command_out, directory, config_file,
|
|
462 |
- source_cache, tempdir, palette,
|
|
463 |
- command_str)
|
|
464 |
- |
|
465 |
- if output is None:
|
|
466 |
- click.echo(converted)
|
|
467 |
- else:
|
|
468 |
- outdir = os.path.dirname(output)
|
|
469 |
- os.makedirs(outdir, exist_ok=True)
|
|
470 |
- with open(output, 'wb') as f:
|
|
471 |
- f.write(converted.encode('utf-8'))
|
|
454 |
+ run_session(description, tempdir, source_cache, palette, config_file, force)
|
|
472 | 455 |
|
473 | 456 |
return 0
|
474 | 457 |
|