Tristan Van Berkom pushed to branch tlater/message-lines at BuildStream / buildstream
Commits:
-
951a8df1
by Angelos Evripiotis at 2019-01-18T18:02:22Z
-
9911023f
by Angelos Evripiotis at 2019-01-18T18:56:21Z
-
c9ce89d2
by Tristan Van Berkom at 2019-01-18T19:36:26Z
-
c536ab6a
by Tristan Van Berkom at 2019-01-18T19:36:26Z
-
99699ffc
by Tristan Van Berkom at 2019-01-18T19:36:26Z
-
8ce483d4
by Tristan Van Berkom at 2019-01-18T19:36:26Z
-
73c7252d
by Tristan Van Berkom at 2019-01-18T20:57:42Z
-
32735414
by Tristan Maat at 2019-01-18T21:29:52Z
-
cc4fb4bd
by Tristan Maat at 2019-01-18T21:29:52Z
7 changed files:
- buildstream/_cas/cascache.py
- buildstream/_frontend/cli.py
- buildstream/_frontend/widget.py
- buildstream/utils.py
- tests/artifactcache/expiry.py
- + tests/integration/messages.py
- tests/testutils/runcli.py
Changes:
... | ... | @@ -21,7 +21,7 @@ import hashlib |
21 | 21 |
import itertools
|
22 | 22 |
import os
|
23 | 23 |
import stat
|
24 |
-import tempfile
|
|
24 |
+import errno
|
|
25 | 25 |
import uuid
|
26 | 26 |
import contextlib
|
27 | 27 |
|
... | ... | @@ -129,7 +129,7 @@ class CASCache(): |
129 | 129 |
else:
|
130 | 130 |
return dest
|
131 | 131 |
|
132 |
- with tempfile.TemporaryDirectory(prefix='tmp', dir=self.tmpdir) as tmpdir:
|
|
132 |
+ with utils._tempdir(prefix='tmp', dir=self.tmpdir) as tmpdir:
|
|
133 | 133 |
checkoutdir = os.path.join(tmpdir, ref)
|
134 | 134 |
self._checkout(checkoutdir, tree)
|
135 | 135 |
|
... | ... | @@ -374,7 +374,7 @@ class CASCache(): |
374 | 374 |
for chunk in iter(lambda: tmp.read(4096), b""):
|
375 | 375 |
h.update(chunk)
|
376 | 376 |
else:
|
377 |
- tmp = stack.enter_context(tempfile.NamedTemporaryFile(dir=self.tmpdir))
|
|
377 |
+ tmp = stack.enter_context(utils._tempnamedfile(dir=self.tmpdir))
|
|
378 | 378 |
# Set mode bits to 0644
|
379 | 379 |
os.chmod(tmp.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
|
380 | 380 |
|
... | ... | @@ -545,11 +545,7 @@ class CASCache(): |
545 | 545 |
def remove(self, ref, *, defer_prune=False):
|
546 | 546 |
|
547 | 547 |
# Remove cache ref
|
548 |
- refpath = self._refpath(ref)
|
|
549 |
- if not os.path.exists(refpath):
|
|
550 |
- raise CASCacheError("Could not find ref '{}'".format(ref))
|
|
551 |
- |
|
552 |
- os.unlink(refpath)
|
|
548 |
+ self._remove_ref(ref)
|
|
553 | 549 |
|
554 | 550 |
if not defer_prune:
|
555 | 551 |
pruned = self.prune()
|
... | ... | @@ -626,6 +622,55 @@ class CASCache(): |
626 | 622 |
def _refpath(self, ref):
|
627 | 623 |
return os.path.join(self.casdir, 'refs', 'heads', ref)
|
628 | 624 |
|
625 |
+ # _remove_ref()
|
|
626 |
+ #
|
|
627 |
+ # Removes a ref.
|
|
628 |
+ #
|
|
629 |
+ # This also takes care of pruning away directories which can
|
|
630 |
+ # be removed after having removed the given ref.
|
|
631 |
+ #
|
|
632 |
+ # Args:
|
|
633 |
+ # ref (str): The ref to remove
|
|
634 |
+ #
|
|
635 |
+ # Raises:
|
|
636 |
+ # (CASCacheError): If the ref didnt exist, or a system error
|
|
637 |
+ # occurred while removing it
|
|
638 |
+ #
|
|
639 |
+ def _remove_ref(self, ref):
|
|
640 |
+ |
|
641 |
+ # Remove the ref itself
|
|
642 |
+ refpath = self._refpath(ref)
|
|
643 |
+ try:
|
|
644 |
+ os.unlink(refpath)
|
|
645 |
+ except FileNotFoundError as e:
|
|
646 |
+ raise CASCacheError("Could not find ref '{}'".format(ref)) from e
|
|
647 |
+ |
|
648 |
+ # Now remove any leading directories
|
|
649 |
+ basedir = os.path.join(self.casdir, 'refs', 'heads')
|
|
650 |
+ components = list(os.path.split(ref))
|
|
651 |
+ while components:
|
|
652 |
+ components.pop()
|
|
653 |
+ refdir = os.path.join(basedir, *components)
|
|
654 |
+ |
|
655 |
+ # Break out once we reach the base
|
|
656 |
+ if refdir == basedir:
|
|
657 |
+ break
|
|
658 |
+ |
|
659 |
+ try:
|
|
660 |
+ os.rmdir(refdir)
|
|
661 |
+ except FileNotFoundError:
|
|
662 |
+ # The parent directory did not exist, but it's
|
|
663 |
+ # parent directory might still be ready to prune
|
|
664 |
+ pass
|
|
665 |
+ except OSError as e:
|
|
666 |
+ if e.errno == errno.ENOTEMPTY:
|
|
667 |
+ # The parent directory was not empty, so we
|
|
668 |
+ # cannot prune directories beyond this point
|
|
669 |
+ break
|
|
670 |
+ |
|
671 |
+ # Something went wrong here
|
|
672 |
+ raise CASCacheError("System error while removing ref '{}': {}".format(ref, e)) from e
|
|
673 |
+ |
|
629 | 674 |
# _commit_directory():
|
630 | 675 |
#
|
631 | 676 |
# Adds local directory to content addressable store.
|
... | ... | @@ -797,7 +842,7 @@ class CASCache(): |
797 | 842 |
# already in local repository
|
798 | 843 |
return objpath
|
799 | 844 |
|
800 |
- with tempfile.NamedTemporaryFile(dir=self.tmpdir) as f:
|
|
845 |
+ with utils._tempnamedfile(dir=self.tmpdir) as f:
|
|
801 | 846 |
remote._fetch_blob(digest, f)
|
802 | 847 |
|
803 | 848 |
added_digest = self.add_object(path=f.name, link_directly=True)
|
... | ... | @@ -807,7 +852,7 @@ class CASCache(): |
807 | 852 |
|
808 | 853 |
def _batch_download_complete(self, batch):
|
809 | 854 |
for digest, data in batch.send():
|
810 |
- with tempfile.NamedTemporaryFile(dir=self.tmpdir) as f:
|
|
855 |
+ with utils._tempnamedfile(dir=self.tmpdir) as f:
|
|
811 | 856 |
f.write(data)
|
812 | 857 |
f.flush()
|
813 | 858 |
|
... | ... | @@ -904,7 +949,7 @@ class CASCache(): |
904 | 949 |
|
905 | 950 |
def _fetch_tree(self, remote, digest):
|
906 | 951 |
# download but do not store the Tree object
|
907 |
- with tempfile.NamedTemporaryFile(dir=self.tmpdir) as out:
|
|
952 |
+ with utils._tempnamedfile(dir=self.tmpdir) as out:
|
|
908 | 953 |
remote._fetch_blob(digest, out)
|
909 | 954 |
|
910 | 955 |
tree = remote_execution_pb2.Tree()
|
... | ... | @@ -554,6 +554,12 @@ def shell(app, element, sysroot, mount, isolate, build_, cli_buildtree, command) |
554 | 554 |
element, assuming it has already been built and all required
|
555 | 555 |
artifacts are in the local cache.
|
556 | 556 |
|
557 |
+ Use '--' to separate a command from the options to bst,
|
|
558 |
+ otherwise bst may respond to them instead. e.g.
|
|
559 |
+ |
|
560 |
+ \b
|
|
561 |
+ bst shell example.bst -- df -h
|
|
562 |
+ |
|
557 | 563 |
Use the --build option to create a temporary sysroot for
|
558 | 564 |
building the element instead.
|
559 | 565 |
|
... | ... | @@ -647,8 +647,9 @@ class LogLine(Widget): |
647 | 647 |
abbrev = False
|
648 | 648 |
if message.message_type not in ERROR_MESSAGES \
|
649 | 649 |
and not frontend_message and n_lines > self._message_lines:
|
650 |
- abbrev = True
|
|
651 | 650 |
lines = lines[0:self._message_lines]
|
651 |
+ if self._message_lines > 0:
|
|
652 |
+ abbrev = True
|
|
652 | 653 |
else:
|
653 | 654 |
lines[n_lines - 1] = lines[n_lines - 1].rstrip('\n')
|
654 | 655 |
|
... | ... | @@ -674,7 +675,7 @@ class LogLine(Widget): |
674 | 675 |
if self.context is not None and not self.context.log_verbose:
|
675 | 676 |
text += self._indent + self._err_profile.fmt("Log file: ")
|
676 | 677 |
text += self._indent + self._logfile_widget.render(message) + '\n'
|
677 |
- else:
|
|
678 |
+ elif self._log_lines > 0:
|
|
678 | 679 |
text += self._indent + self._err_profile.fmt("Printing the last {} lines from log file:"
|
679 | 680 |
.format(self._log_lines)) + '\n'
|
680 | 681 |
text += self._indent + self._logfile_widget.render(message, abbrev=False) + '\n'
|
... | ... | @@ -1032,6 +1032,36 @@ def _tempdir(suffix="", prefix="tmp", dir=None): # pylint: disable=redefined-bu |
1032 | 1032 |
cleanup_tempdir()
|
1033 | 1033 |
|
1034 | 1034 |
|
1035 |
+# _tempnamedfile()
|
|
1036 |
+#
|
|
1037 |
+# A context manager for doing work on an open temporary file
|
|
1038 |
+# which is guaranteed to be named and have an entry in the filesystem.
|
|
1039 |
+#
|
|
1040 |
+# Args:
|
|
1041 |
+# dir (str): A path to a parent directory for the temporary file
|
|
1042 |
+# suffix (str): A suffix for the temproary file name
|
|
1043 |
+# prefix (str): A prefix for the temporary file name
|
|
1044 |
+#
|
|
1045 |
+# Yields:
|
|
1046 |
+# (str): The temporary file handle
|
|
1047 |
+#
|
|
1048 |
+# Do not use tempfile.NamedTemporaryFile() directly, as this will
|
|
1049 |
+# leak files on the filesystem when BuildStream exits a process
|
|
1050 |
+# on SIGTERM.
|
|
1051 |
+#
|
|
1052 |
+@contextmanager
|
|
1053 |
+def _tempnamedfile(suffix="", prefix="tmp", dir=None): # pylint: disable=redefined-builtin
|
|
1054 |
+ temp = None
|
|
1055 |
+ |
|
1056 |
+ def close_tempfile():
|
|
1057 |
+ if temp is not None:
|
|
1058 |
+ temp.close()
|
|
1059 |
+ |
|
1060 |
+ with _signals.terminator(close_tempfile), \
|
|
1061 |
+ tempfile.NamedTemporaryFile(suffix=suffix, prefix=prefix, dir=dir) as temp:
|
|
1062 |
+ yield temp
|
|
1063 |
+ |
|
1064 |
+ |
|
1035 | 1065 |
# _kill_process_tree()
|
1036 | 1066 |
#
|
1037 | 1067 |
# Brutally murder a process and all of its children
|
... | ... | @@ -382,6 +382,7 @@ def test_extract_expiry(cli, datafiles, tmpdir): |
382 | 382 |
res = cli.run(project=project, args=['checkout', 'target.bst', os.path.join(str(tmpdir), 'checkout')])
|
383 | 383 |
res.assert_success()
|
384 | 384 |
|
385 |
+ # Get a snapshot of the extracts in advance
|
|
385 | 386 |
extractdir = os.path.join(project, 'cache', 'artifacts', 'extract', 'test', 'target')
|
386 | 387 |
extracts = os.listdir(extractdir)
|
387 | 388 |
assert(len(extracts) == 1)
|
... | ... | @@ -395,3 +396,16 @@ def test_extract_expiry(cli, datafiles, tmpdir): |
395 | 396 |
|
396 | 397 |
# Now the extract should be removed.
|
397 | 398 |
assert not os.path.exists(extract)
|
399 |
+ |
|
400 |
+ # As an added bonus, let's ensure that no directories have been left behind
|
|
401 |
+ #
|
|
402 |
+ # Now we should have a directory for the cached target2.bst, which
|
|
403 |
+ # replaced target.bst in the cache, we should not have a directory
|
|
404 |
+ # for the target.bst
|
|
405 |
+ refsdir = os.path.join(project, 'cache', 'artifacts', 'cas', 'refs', 'heads')
|
|
406 |
+ refsdirtest = os.path.join(refsdir, 'test')
|
|
407 |
+ refsdirtarget = os.path.join(refsdirtest, 'target')
|
|
408 |
+ refsdirtarget2 = os.path.join(refsdirtest, 'target2')
|
|
409 |
+ |
|
410 |
+ assert os.path.isdir(refsdirtarget2)
|
|
411 |
+ assert not os.path.exists(refsdirtarget)
|
1 |
+#
|
|
2 |
+# Copyright (C) 2018 Codethink Limited
|
|
3 |
+#
|
|
4 |
+# This program is free software; you can redistribute it and/or
|
|
5 |
+# modify it under the terms of the GNU Lesser General Public
|
|
6 |
+# License as published by the Free Software Foundation; either
|
|
7 |
+# version 2 of the License, or (at your option) any later version.
|
|
8 |
+#
|
|
9 |
+# This library is distributed in the hope that it will be useful,
|
|
10 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12 |
+# Lesser General Public License for more details.
|
|
13 |
+#
|
|
14 |
+# You should have received a copy of the GNU Lesser General Public
|
|
15 |
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
16 |
+#
|
|
17 |
+# Authors: Tristan Maat <tristan maat codethink co uk>
|
|
18 |
+#
|
|
19 |
+ |
|
20 |
+import os
|
|
21 |
+import pytest
|
|
22 |
+ |
|
23 |
+from buildstream import _yaml
|
|
24 |
+from buildstream._exceptions import ErrorDomain
|
|
25 |
+ |
|
26 |
+from tests.testutils import cli_integration as cli
|
|
27 |
+from tests.testutils.site import HAVE_BWRAP, IS_LINUX
|
|
28 |
+ |
|
29 |
+ |
|
30 |
+pytestmark = pytest.mark.integration
|
|
31 |
+ |
|
32 |
+ |
|
33 |
+# Project directory
|
|
34 |
+DATA_DIR = os.path.join(
|
|
35 |
+ os.path.dirname(os.path.realpath(__file__)),
|
|
36 |
+ "project",
|
|
37 |
+)
|
|
38 |
+ |
|
39 |
+ |
|
40 |
+@pytest.mark.integration
|
|
41 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
42 |
+@pytest.mark.skipif(IS_LINUX and not HAVE_BWRAP, reason='Only available with bubblewrap on Linux')
|
|
43 |
+def test_disable_message_lines(cli, tmpdir, datafiles):
|
|
44 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
45 |
+ element_path = os.path.join(project, 'elements')
|
|
46 |
+ element_name = 'message.bst'
|
|
47 |
+ |
|
48 |
+ element = {
|
|
49 |
+ 'kind': 'manual',
|
|
50 |
+ 'depends': [{
|
|
51 |
+ 'filename': 'base.bst'
|
|
52 |
+ }],
|
|
53 |
+ 'config': {
|
|
54 |
+ 'build-commands':
|
|
55 |
+ ['echo "Silly message"'],
|
|
56 |
+ 'strip-commands': []
|
|
57 |
+ }
|
|
58 |
+ }
|
|
59 |
+ |
|
60 |
+ os.makedirs(os.path.dirname(os.path.join(element_path, element_name)), exist_ok=True)
|
|
61 |
+ _yaml.dump(element, os.path.join(element_path, element_name))
|
|
62 |
+ |
|
63 |
+ # First we check that we get the "Silly message"
|
|
64 |
+ result = cli.run(project=project, args=["build", element_name])
|
|
65 |
+ result.assert_success()
|
|
66 |
+ assert 'echo "Silly message"' in result.stderr
|
|
67 |
+ |
|
68 |
+ # Let's now build it again, but with --message-lines 0
|
|
69 |
+ cli.remove_artifact_from_cache(project, element_name)
|
|
70 |
+ result = cli.run(project=project, args=["--message-lines", "0",
|
|
71 |
+ "build", element_name])
|
|
72 |
+ result.assert_success()
|
|
73 |
+ assert "Message contains " not in result.stderr
|
|
74 |
+ |
|
75 |
+ |
|
76 |
+@pytest.mark.integration
|
|
77 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
78 |
+@pytest.mark.skipif(IS_LINUX and not HAVE_BWRAP, reason='Only available with bubblewrap on Linux')
|
|
79 |
+def test_disable_error_lines(cli, tmpdir, datafiles):
|
|
80 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
81 |
+ element_path = os.path.join(project, 'elements')
|
|
82 |
+ element_name = 'message.bst'
|
|
83 |
+ |
|
84 |
+ element = {
|
|
85 |
+ 'kind': 'manual',
|
|
86 |
+ 'depends': [{
|
|
87 |
+ 'filename': 'base.bst'
|
|
88 |
+ }],
|
|
89 |
+ 'config': {
|
|
90 |
+ 'build-commands':
|
|
91 |
+ ['This is a syntax error > >'],
|
|
92 |
+ 'strip-commands': []
|
|
93 |
+ }
|
|
94 |
+ }
|
|
95 |
+ |
|
96 |
+ os.makedirs(os.path.dirname(os.path.join(element_path, element_name)), exist_ok=True)
|
|
97 |
+ _yaml.dump(element, os.path.join(element_path, element_name))
|
|
98 |
+ |
|
99 |
+ # First we check that we get the syntax error
|
|
100 |
+ result = cli.run(project=project, args=["--error-lines", "0",
|
|
101 |
+ "build", element_name])
|
|
102 |
+ result.assert_main_error(ErrorDomain.STREAM, None)
|
|
103 |
+ assert "This is a syntax error" in result.stderr
|
|
104 |
+ |
|
105 |
+ # Let's now build it again, but with --error-lines 0
|
|
106 |
+ cli.remove_artifact_from_cache(project, element_name)
|
|
107 |
+ result = cli.run(project=project, args=["--error-lines", "0",
|
|
108 |
+ "build", element_name])
|
|
109 |
+ result.assert_main_error(ErrorDomain.STREAM, None)
|
|
110 |
+ assert "Printing the last" not in result.stderr
|
... | ... | @@ -245,8 +245,14 @@ class Cli(): |
245 | 245 |
|
246 | 246 |
def remove_artifact_from_cache(self, project, element_name,
|
247 | 247 |
*, cache_dir=None):
|
248 |
+ # Read configuration to figure out where artifacts are stored
|
|
248 | 249 |
if not cache_dir:
|
249 |
- cache_dir = os.path.join(project, 'cache', 'artifacts')
|
|
250 |
+ default = os.path.join(project, 'cache', 'artifacts')
|
|
251 |
+ |
|
252 |
+ if self.config is not None:
|
|
253 |
+ cache_dir = self.config.get('artifactdir', default)
|
|
254 |
+ else:
|
|
255 |
+ cache_dir = default
|
|
250 | 256 |
|
251 | 257 |
cache_dir = os.path.join(cache_dir, 'cas', 'refs', 'heads')
|
252 | 258 |
|