Qinusty pushed to branch Qinusty/600-recursive-variables at BuildStream / buildstream
Commits:
- 
10eeb503
by Javier Jardón at 2018-08-28T22:22:00Z
- 
d3c32ca2
by Javier Jardón at 2018-08-28T22:22:00Z
- 
3ae5fd05
by Javier Jardón at 2018-08-28T22:22:00Z
- 
c6fb5ba7
by Javier Jardón at 2018-08-28T22:22:00Z
- 
c5eca59d
by Tristan Van Berkom at 2018-08-29T10:45:33Z
- 
63c6ee72
by Tristan Van Berkom at 2018-08-29T10:46:30Z
- 
2d527052
by Tristan Van Berkom at 2018-08-29T10:46:30Z
- 
29e7eea8
by Tristan Van Berkom at 2018-08-29T10:46:30Z
- 
5d508779
by Tristan Van Berkom at 2018-08-29T11:13:53Z
- 
323a5403
by Chandan Singh at 2018-08-29T12:13:28Z
- 
5fc7b5ac
by Tristan Van Berkom at 2018-08-29T12:37:08Z
- 
42ad937d
by Josh Smith at 2018-08-29T12:38:15Z
- 
aa3a33b3
by Tristan Van Berkom at 2018-08-29T13:03:58Z
- 
2992a675
by Josh Smith at 2018-08-29T14:35:19Z
- 
601180ac
by Josh Smith at 2018-08-29T15:36:09Z
26 changed files:
- .gitignore
- .gitlab-ci.yml
- README.rst
- buildstream/_exceptions.py
- buildstream/_variables.py
- buildstream/plugins/sources/tar.py
- buildstream/plugins/sources/zip.py
- buildstream/source.py
- buildstream/utils.py
- dev-requirements.txt
- doc/Makefile
- + doc/badges.py
- doc/bst2html.py
- doc/source/conf.py
- doc/source/install_versions.rst
- doc/source/main_install.rst
- + doc/source/release-badge.rst
- + doc/source/snapshot-badge.rst
- setup.py
- tests/cachekey/cachekey.py
- tests/cachekey/update.py
- tests/format/variables.py
- + tests/format/variables/cyclic_variables/cyclic.bst
- + tests/format/variables/cyclic_variables/project.conf
- tests/frontend/buildcheckout.py
- tests/testutils/runcli.py
Changes:
| ... | ... | @@ -26,6 +26,7 @@ __pycache__/ | 
| 26 | 26 |  buildstream/__version__.py
 | 
| 27 | 27 |  | 
| 28 | 28 |  # Autogenerated doc
 | 
| 29 | +doc/source/badges/
 | |
| 29 | 30 |  doc/source/sessions/
 | 
| 30 | 31 |  doc/source/elements/
 | 
| 31 | 32 |  doc/source/sources/
 | 
| ... | ... | @@ -84,25 +84,25 @@ source_dist: | 
| 84 | 84 |      - coverage-linux/
 | 
| 85 | 85 |  | 
| 86 | 86 |  tests-debian-9:
 | 
| 87 | -  image: buildstream/testsuite-debian:9-master-114-4cab18e3
 | |
| 87 | +  image: buildstream/testsuite-debian:9-master-117-aa3a33b3
 | |
| 88 | 88 |    <<: *linux-tests
 | 
| 89 | 89 |  | 
| 90 | 90 |  tests-fedora-27:
 | 
| 91 | -  image: buildstream/testsuite-fedora:27-master-114-4cab18e3
 | |
| 91 | +  image: buildstream/testsuite-fedora:27-master-117-aa3a33b3
 | |
| 92 | 92 |    <<: *linux-tests
 | 
| 93 | 93 |  | 
| 94 | 94 |  tests-fedora-28:
 | 
| 95 | -  image: buildstream/testsuite-fedora:28-master-114-4cab18e3
 | |
| 95 | +  image: buildstream/testsuite-fedora:28-master-117-aa3a33b3
 | |
| 96 | 96 |    <<: *linux-tests
 | 
| 97 | 97 |  | 
| 98 | 98 |  tests-ubuntu-18.04:
 | 
| 99 | -  image: buildstream/testsuite-ubuntu:18.04-master-114-4cab18e3
 | |
| 99 | +  image: buildstream/testsuite-ubuntu:18.04-master-117-aa3a33b3
 | |
| 100 | 100 |    <<: *linux-tests
 | 
| 101 | 101 |  | 
| 102 | 102 |  tests-unix:
 | 
| 103 | 103 |    # Use fedora here, to a) run a test on fedora and b) ensure that we
 | 
| 104 | 104 |    # can get rid of ostree - this is not possible with debian-8
 | 
| 105 | -  image: buildstream/testsuite-fedora:27-master-114-4cab18e3
 | |
| 105 | +  image: buildstream/testsuite-fedora:27-master-117-aa3a33b3
 | |
| 106 | 106 |    stage: test
 | 
| 107 | 107 |    variables:
 | 
| 108 | 108 |      BST_FORCE_BACKEND: "unix"
 | 
| 1 | 1 |  About
 | 
| 2 | 2 |  -----
 | 
| 3 | + | |
| 4 | +.. image:: https://buildstream.gitlab.io/buildstream/_static/release.svg
 | |
| 5 | +   :target: https://gitlab.com/BuildStream/buildstream/commits/bst-1.2
 | |
| 6 | + | |
| 7 | +.. image:: https://buildstream.gitlab.io/buildstream/_static/snapshot.svg
 | |
| 8 | +   :target: https://gitlab.com/BuildStream/buildstream/commits/master
 | |
| 9 | + | |
| 3 | 10 |  .. image:: https://gitlab.com/BuildStream/buildstream/badges/master/pipeline.svg
 | 
| 4 | 11 |     :target: https://gitlab.com/BuildStream/buildstream/commits/master
 | 
| 5 | 12 |  | 
| ... | ... | @@ -217,6 +217,9 @@ class LoadErrorReason(Enum): | 
| 217 | 217 |      # A recursive include has been encountered.
 | 
| 218 | 218 |      RECURSIVE_INCLUDE = 21
 | 
| 219 | 219 |  | 
| 220 | +    # A recursive variable has been encountered
 | |
| 221 | +    RECURSIVE_VARIABLE = 22
 | |
| 222 | + | |
| 220 | 223 |  | 
| 221 | 224 |  # LoadError
 | 
| 222 | 225 |  #
 | 
| ... | ... | @@ -61,7 +61,7 @@ class Variables(): | 
| 61 | 61 |      #    LoadError, if the string contains unresolved variable references.
 | 
| 62 | 62 |      #
 | 
| 63 | 63 |      def subst(self, string):
 | 
| 64 | -        substitute, unmatched = self._subst(string, self.variables)
 | |
| 64 | +        substitute, unmatched, _ = self._subst(string, self.variables)
 | |
| 65 | 65 |          unmatched = list(set(unmatched))
 | 
| 66 | 66 |          if unmatched:
 | 
| 67 | 67 |              if len(unmatched) == 1:
 | 
| ... | ... | @@ -82,6 +82,7 @@ class Variables(): | 
| 82 | 82 |          def subst_callback(match):
 | 
| 83 | 83 |              nonlocal variables
 | 
| 84 | 84 |              nonlocal unmatched
 | 
| 85 | +            nonlocal matched
 | |
| 85 | 86 |  | 
| 86 | 87 |              token = match.group(0)
 | 
| 87 | 88 |              varname = match.group(1)
 | 
| ... | ... | @@ -91,6 +92,7 @@ class Variables(): | 
| 91 | 92 |                  # We have to check if the inner string has variables
 | 
| 92 | 93 |                  # and return unmatches for those
 | 
| 93 | 94 |                  unmatched += re.findall(_VARIABLE_MATCH, value)
 | 
| 95 | +                matched += [varname]
 | |
| 94 | 96 |              else:
 | 
| 95 | 97 |                  # Return unmodified token
 | 
| 96 | 98 |                  unmatched += [varname]
 | 
| ... | ... | @@ -98,10 +100,11 @@ class Variables(): | 
| 98 | 100 |  | 
| 99 | 101 |              return value
 | 
| 100 | 102 |  | 
| 103 | +        matched = []
 | |
| 101 | 104 |          unmatched = []
 | 
| 102 | 105 |          replacement = re.sub(_VARIABLE_MATCH, subst_callback, string)
 | 
| 103 | 106 |  | 
| 104 | -        return (replacement, unmatched)
 | |
| 107 | +        return (replacement, unmatched, matched)
 | |
| 105 | 108 |  | 
| 106 | 109 |      # Variable resolving code
 | 
| 107 | 110 |      #
 | 
| ... | ... | @@ -131,7 +134,15 @@ class Variables(): | 
| 131 | 134 |                  # Ensure stringness of the value before substitution
 | 
| 132 | 135 |                  value = _yaml.node_get(variables, str, key)
 | 
| 133 | 136 |  | 
| 134 | -                resolved_var, item_unmatched = self._subst(value, variables)
 | |
| 137 | +                resolved_var, item_unmatched, matched = self._subst(value, variables)
 | |
| 138 | + | |
| 139 | +                if _wrap_variable(key) in resolved_var:
 | |
| 140 | +                    referenced_through = find_recursive_variable(key, matched, variables)
 | |
| 141 | +                    raise LoadError(LoadErrorReason.RECURSIVE_VARIABLE,
 | |
| 142 | +                                    "{}: ".format(_yaml.node_get_provenance(variables, key)) +
 | |
| 143 | +                                    ("Variable '{}' expands to contain a reference to itself. " +
 | |
| 144 | +                                     "Perhaps '{}' contains '{}").format(key, referenced_through, _wrap_variable(key)))
 | |
| 145 | + | |
| 135 | 146 |                  resolved[key] = resolved_var
 | 
| 136 | 147 |                  unmatched += item_unmatched
 | 
| 137 | 148 |  | 
| ... | ... | @@ -168,8 +179,21 @@ class Variables(): | 
| 168 | 179 |      # Helper function to fetch information about the node referring to a variable
 | 
| 169 | 180 |      #
 | 
| 170 | 181 |      def _find_references(self, varname):
 | 
| 171 | -        fullname = '%{' + varname + '}'
 | |
| 182 | +        fullname = _wrap_variable(varname)
 | |
| 172 | 183 |          for key, value in _yaml.node_items(self.original):
 | 
| 173 | 184 |              if fullname in value:
 | 
| 174 | 185 |                  provenance = _yaml.node_get_provenance(self.original, key)
 | 
| 175 | 186 |                  yield (key, provenance)
 | 
| 187 | + | |
| 188 | + | |
| 189 | +def find_recursive_variable(variable, matched_variables, all_vars):
 | |
| 190 | +    matched_values = (_yaml.node_get(all_vars, str, key) for key in matched_variables)
 | |
| 191 | +    for key, value in zip(matched_variables, matched_values):
 | |
| 192 | +        if _wrap_variable(variable) in value:
 | |
| 193 | +            return key
 | |
| 194 | +    else:
 | |
| 195 | +        return None
 | |
| 196 | + | |
| 197 | + | |
| 198 | +def _wrap_variable(var):
 | |
| 199 | +    return "%{" + var + "}" | 
| ... | ... | @@ -127,7 +127,7 @@ class TarSource(DownloadableFileSource): | 
| 127 | 127 |          if not base_dir.endswith(os.sep):
 | 
| 128 | 128 |              base_dir = base_dir + os.sep
 | 
| 129 | 129 |  | 
| 130 | -        l = len(base_dir)
 | |
| 130 | +        L = len(base_dir)
 | |
| 131 | 131 |          for member in tar.getmembers():
 | 
| 132 | 132 |  | 
| 133 | 133 |              # First, ensure that a member never starts with `./`
 | 
| ... | ... | @@ -145,9 +145,9 @@ class TarSource(DownloadableFileSource): | 
| 145 | 145 |                  #       base directory.
 | 
| 146 | 146 |                  #
 | 
| 147 | 147 |                  if member.type == tarfile.LNKTYPE:
 | 
| 148 | -                    member.linkname = member.linkname[l:]
 | |
| 148 | +                    member.linkname = member.linkname[L:]
 | |
| 149 | 149 |  | 
| 150 | -                member.path = member.path[l:]
 | |
| 150 | +                member.path = member.path[L:]
 | |
| 151 | 151 |                  yield member
 | 
| 152 | 152 |  | 
| 153 | 153 |      # We want to iterate over all paths of a tarball, but getmembers()
 | 
| ... | ... | @@ -121,13 +121,13 @@ class ZipSource(DownloadableFileSource): | 
| 121 | 121 |          if not base_dir.endswith(os.sep):
 | 
| 122 | 122 |              base_dir = base_dir + os.sep
 | 
| 123 | 123 |  | 
| 124 | -        l = len(base_dir)
 | |
| 124 | +        L = len(base_dir)
 | |
| 125 | 125 |          for member in archive.infolist():
 | 
| 126 | 126 |              if member.filename == base_dir:
 | 
| 127 | 127 |                  continue
 | 
| 128 | 128 |  | 
| 129 | 129 |              if member.filename.startswith(base_dir):
 | 
| 130 | -                member.filename = member.filename[l:]
 | |
| 130 | +                member.filename = member.filename[L:]
 | |
| 131 | 131 |                  yield member
 | 
| 132 | 132 |  | 
| 133 | 133 |      # We want to iterate over all paths of an archive, but namelist()
 | 
| ... | ... | @@ -219,10 +219,7 @@ class SourceFetcher(): | 
| 219 | 219 |          Args:
 | 
| 220 | 220 |             url (str): The url used to download.
 | 
| 221 | 221 |          """
 | 
| 222 | -        # Not guaranteed to be a valid alias yet.
 | |
| 223 | -        # Ensuring it's a valid alias currently happens in Project.get_alias_uris
 | |
| 224 | -        alias, _ = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 225 | -        self.__alias = alias
 | |
| 222 | +        self.__alias = _extract_alias(url)
 | |
| 226 | 223 |  | 
| 227 | 224 |      #############################################################
 | 
| 228 | 225 |      #            Private Methods used in BuildStream            #
 | 
| ... | ... | @@ -459,8 +456,7 @@ class Source(Plugin): | 
| 459 | 456 |  | 
| 460 | 457 |          *Since: 1.2*
 | 
| 461 | 458 |          """
 | 
| 462 | -        alias, _ = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 463 | -        self.__expected_alias = alias
 | |
| 459 | +        self.__expected_alias = _extract_alias(url)
 | |
| 464 | 460 |  | 
| 465 | 461 |      def get_source_fetchers(self):
 | 
| 466 | 462 |          """Get the objects that are used for fetching
 | 
| ... | ... | @@ -525,8 +521,7 @@ class Source(Plugin): | 
| 525 | 521 |          else:
 | 
| 526 | 522 |              # Sneakily store the alias if it hasn't already been stored
 | 
| 527 | 523 |              if not self.__expected_alias and url and utils._ALIAS_SEPARATOR in url:
 | 
| 528 | -                url_alias, _ = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 529 | -                self.__expected_alias = url_alias
 | |
| 524 | +                self.mark_download_url(url)
 | |
| 530 | 525 |  | 
| 531 | 526 |              project = self._get_project()
 | 
| 532 | 527 |              return project.translate_url(url, first_pass=self.__first_pass)
 | 
| ... | ... | @@ -914,12 +909,12 @@ class Source(Plugin): | 
| 914 | 909 |      # Tries to call track for every mirror, stopping once it succeeds
 | 
| 915 | 910 |      def __do_track(self, **kwargs):
 | 
| 916 | 911 |          project = self._get_project()
 | 
| 917 | -        # If there are no mirrors, or no aliases to replace, there's nothing to do here.
 | |
| 918 | 912 |          alias = self._get_alias()
 | 
| 919 | 913 |          if self.__first_pass:
 | 
| 920 | 914 |              mirrors = project.first_pass_config.mirrors
 | 
| 921 | 915 |          else:
 | 
| 922 | 916 |              mirrors = project.config.mirrors
 | 
| 917 | +        # If there are no mirrors, or no aliases to replace, there's nothing to do here.
 | |
| 923 | 918 |          if not mirrors or not alias:
 | 
| 924 | 919 |              return self.track(**kwargs)
 | 
| 925 | 920 |  | 
| ... | ... | @@ -988,3 +983,11 @@ class Source(Plugin): | 
| 988 | 983 |  | 
| 989 | 984 |              if src.get_consistency() == Consistency.RESOLVED:
 | 
| 990 | 985 |                  src._fetch(previous_sources[0:index])
 | 
| 986 | + | |
| 987 | + | |
| 988 | +def _extract_alias(url):
 | |
| 989 | +    parts = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 990 | +    if len(parts) > 1 and not parts[0].lower() in utils._URI_SCHEMES:
 | |
| 991 | +        return parts[0]
 | |
| 992 | +    else:
 | |
| 993 | +        return "" | 
| ... | ... | @@ -47,6 +47,7 @@ _magic_timestamp = calendar.timegm([2011, 11, 11, 11, 11, 11]) | 
| 47 | 47 |  | 
| 48 | 48 |  # The separator we use for user specified aliases
 | 
| 49 | 49 |  _ALIAS_SEPARATOR = ':'
 | 
| 50 | +_URI_SCHEMES = ["http", "https", "ftp", "file", "git", "sftp", "ssh"]
 | |
| 50 | 51 |  | 
| 51 | 52 |  | 
| 52 | 53 |  class UtilError(BstError):
 | 
| ... | ... | @@ -645,6 +646,7 @@ def _pretty_size(size, dec_places=0): | 
| 645 | 646 |              psize /= 1024
 | 
| 646 | 647 |      return "{size:g}{unit}".format(size=round(psize, dec_places), unit=unit)
 | 
| 647 | 648 |  | 
| 649 | + | |
| 648 | 650 |  # A sentinel to be used as a default argument for functions that need
 | 
| 649 | 651 |  # to distinguish between a kwarg set to None and an unset kwarg.
 | 
| 650 | 652 |  _sentinel = object()
 | 
| ... | ... | @@ -8,3 +8,4 @@ pytest-env | 
| 8 | 8 |  pytest-pep8
 | 
| 9 | 9 |  pytest-pylint
 | 
| 10 | 10 |  pytest-xdist
 | 
| 11 | +pytest-timeout | 
| ... | ... | @@ -35,7 +35,7 @@ endif | 
| 35 | 35 |  PYTHONPATH=$(CURDIR)/..:$(CURDIR)/../buildstream/plugins
 | 
| 36 | 36 |  | 
| 37 | 37 |  | 
| 38 | -.PHONY: all clean templates templates-clean sessions sessions-prep sessions-clean html devhelp
 | |
| 38 | +.PHONY: all clean templates templates-clean sessions sessions-prep sessions-clean badges badges-clean html devhelp
 | |
| 39 | 39 |  | 
| 40 | 40 |  # Canned recipe for generating plugin api skeletons
 | 
| 41 | 41 |  #   $1 = the plugin directory
 | 
| ... | ... | @@ -70,9 +70,13 @@ endef | 
| 70 | 70 |  | 
| 71 | 71 |  all: html devhelp
 | 
| 72 | 72 |  | 
| 73 | -clean: templates-clean sessions-clean
 | |
| 73 | +clean: templates-clean sessions-clean badges-clean
 | |
| 74 | 74 |  	rm -rf build
 | 
| 75 | 75 |  | 
| 76 | +############################################################
 | |
| 77 | +#                 Plugin doc templates                     #
 | |
| 78 | +############################################################
 | |
| 79 | + | |
| 76 | 80 |  # Generate rst templates for the docs using a mix of sphinx-apidoc and
 | 
| 77 | 81 |  # our 'plugin-doc-skeleton' routine for plugin pages.
 | 
| 78 | 82 |  templates:
 | 
| ... | ... | @@ -86,6 +90,10 @@ templates-clean: | 
| 86 | 90 |  	rm -rf source/elements
 | 
| 87 | 91 |  	rm -rf source/sources
 | 
| 88 | 92 |  | 
| 93 | +############################################################
 | |
| 94 | +#                   Session captures                       #
 | |
| 95 | +############################################################
 | |
| 96 | + | |
| 89 | 97 |  # Stage the stored sessions into the place where they will
 | 
| 90 | 98 |  # be used in the build.
 | 
| 91 | 99 |  #
 | 
| ... | ... | @@ -111,10 +119,27 @@ sessions: sessions-prep | 
| 111 | 119 |  sessions-clean:
 | 
| 112 | 120 |  	rm -rf source/sessions
 | 
| 113 | 121 |  | 
| 122 | + | |
| 123 | +############################################################
 | |
| 124 | +#                  Generate release badges                 #
 | |
| 125 | +############################################################
 | |
| 126 | +badges-clean:
 | |
| 127 | +	rm -rf source/badges
 | |
| 128 | + | |
| 129 | +badges:
 | |
| 130 | +	mkdir -p source/badges
 | |
| 131 | +	$(CURDIR)/badges.py > source/badges/snapshot.svg
 | |
| 132 | +	$(CURDIR)/badges.py --release > source/badges/release.svg
 | |
| 133 | + | |
| 134 | + | |
| 135 | +############################################################
 | |
| 136 | +#                    Main sphinx build                     #
 | |
| 137 | +############################################################
 | |
| 138 | + | |
| 114 | 139 |  # Targets which generate docs with sphinx build
 | 
| 115 | 140 |  #
 | 
| 116 | 141 |  #
 | 
| 117 | -html devhelp: templates sessions
 | |
| 142 | +html devhelp: templates sessions badges
 | |
| 118 | 143 |  	@echo "Building $@..."
 | 
| 119 | 144 |  	PYTHONPATH=$(PYTHONPATH) \
 | 
| 120 | 145 |  	    $(SPHINXBUILD) -b $@ $(ALLSPHINXOPTS) "$(BUILDDIR)/$@" \
 | 
| 1 | +#!/usr/bin/env python3
 | |
| 2 | +#
 | |
| 3 | +#  Copyright (C) 2018 Codethink Limited
 | |
| 4 | +#
 | |
| 5 | +#  This program is free software; you can redistribute it and/or
 | |
| 6 | +#  modify it under the terms of the GNU Lesser General Public
 | |
| 7 | +#  License as published by the Free Software Foundation; either
 | |
| 8 | +#  version 2 of the License, or (at your option) any later version.
 | |
| 9 | +#
 | |
| 10 | +#  This library is distributed in the hope that it will be useful,
 | |
| 11 | +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| 12 | +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
 | |
| 13 | +#  Lesser General Public License for more details.
 | |
| 14 | +#
 | |
| 15 | +#  You should have received a copy of the GNU Lesser General Public
 | |
| 16 | +#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
 | |
| 17 | +#
 | |
| 18 | +#  Authors:
 | |
| 19 | +#        Tristan Van Berkom <tristan vanberkom codethink co uk>
 | |
| 20 | +#
 | |
| 21 | +import click
 | |
| 22 | +import subprocess
 | |
| 23 | +import re
 | |
| 24 | + | |
| 25 | +# The badge template is modeled after the gitlab badge svgs
 | |
| 26 | +#
 | |
| 27 | +BADGE_TEMPLATE = """
 | |
| 28 | +<svg xmlns="http://www.w3.org/2000/svg"
 | |
| 29 | +     xmlns:xlink="http://www.w3.org/1999/xlink"
 | |
| 30 | +     width="116" height="20">
 | |
| 31 | +  <a xlink:href="">
 | |
| 32 | +    <linearGradient id="{badge_name}_b" x2="0" y2="100%">
 | |
| 33 | +      <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
 | |
| 34 | +      <stop offset="1" stop-opacity=".1"/>
 | |
| 35 | +    </linearGradient>
 | |
| 36 | + | |
| 37 | +    <mask id="{badge_name}_a">
 | |
| 38 | +      <rect width="116" height="20" rx="3" fill="#fff"/>
 | |
| 39 | +    </mask>
 | |
| 40 | + | |
| 41 | +    <g mask="url(#{badge_name}_a)">
 | |
| 42 | +      <path fill="#555"
 | |
| 43 | +            d="M0 0 h62 v20 H0 z"/>
 | |
| 44 | +      <path fill="{color}"
 | |
| 45 | +            d="M62 0 h54 v20 H62 z"/>
 | |
| 46 | +      <path fill="url(#{badge_name}_b)"
 | |
| 47 | +            d="M0 0 h116 v20 H0 z"/>
 | |
| 48 | +    </g>
 | |
| 49 | + | |
| 50 | +    <g fill="#fff" text-anchor="middle">
 | |
| 51 | +      <g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
 | |
| 52 | +        <text x="31" y="15" fill="#010101" fill-opacity=".3">
 | |
| 53 | +          {badge_name}
 | |
| 54 | +        </text>
 | |
| 55 | +        <text x="31" y="14">
 | |
| 56 | +          {badge_name}
 | |
| 57 | +        </text>
 | |
| 58 | +        <text x="89" y="15" fill="#010101" fill-opacity=".3">
 | |
| 59 | +          {version}
 | |
| 60 | +        </text>
 | |
| 61 | +        <text x="89" y="14">
 | |
| 62 | +          {version}
 | |
| 63 | +        </text>
 | |
| 64 | +      </g>
 | |
| 65 | +    </g>
 | |
| 66 | +  </a>
 | |
| 67 | +</svg>
 | |
| 68 | +"""
 | |
| 69 | + | |
| 70 | +URL_FORMAT = 'https://download.gnome.org/sources/BuildStream/{brief_version}/BuildStream-{full_version}.tar.xz'
 | |
| 71 | +RELEASE_COLOR = '#0040FF'
 | |
| 72 | +SNAPSHOT_COLOR = '#FF8000'
 | |
| 73 | +VERSION_TAG_MATCH = r'([0-9]*)\.([0-9]*)\.([0-9]*)'
 | |
| 74 | + | |
| 75 | + | |
| 76 | +# Parse a release tag and return a three tuple
 | |
| 77 | +# of the major, minor and micro version.
 | |
| 78 | +#
 | |
| 79 | +# Tags which do not follow the release tag format
 | |
| 80 | +# will just be returned as (0, 0, 0)
 | |
| 81 | +#
 | |
| 82 | +def parse_tag(tag):
 | |
| 83 | +    match = re.search(VERSION_TAG_MATCH, tag)
 | |
| 84 | +    if match:
 | |
| 85 | +        major = match.group(1)
 | |
| 86 | +        minor = match.group(2)
 | |
| 87 | +        micro = match.group(3)
 | |
| 88 | +        return (int(major), int(minor), int(micro))
 | |
| 89 | + | |
| 90 | +    return (0, 0, 0)
 | |
| 91 | + | |
| 92 | + | |
| 93 | +# Call out to git and guess the latest version,
 | |
| 94 | +# this will just return (0, 0, 0) in case of any error.
 | |
| 95 | +#
 | |
| 96 | +def guess_version(release):
 | |
| 97 | +    try:
 | |
| 98 | +        tags_output = subprocess.check_output(['git', 'tag'])
 | |
| 99 | +    except CalledProcessError:
 | |
| 100 | +        return (0, 0, 0)
 | |
| 101 | + | |
| 102 | +    # Parse the `git tag` output into a list of integer tuples
 | |
| 103 | +    tags_output = tags_output.decode('UTF-8')
 | |
| 104 | +    all_tags = tags_output.splitlines()
 | |
| 105 | +    all_versions = [parse_tag(tag) for tag in all_tags]
 | |
| 106 | + | |
| 107 | +    # Filter the list by the minor point version, if
 | |
| 108 | +    # we are checking for the latest "release" version, then
 | |
| 109 | +    # only pickup even number minor points.
 | |
| 110 | +    #
 | |
| 111 | +    filtered_versions = [
 | |
| 112 | +        version for version in all_versions
 | |
| 113 | +        if (version[1] % 2) == (not release)
 | |
| 114 | +    ]
 | |
| 115 | + | |
| 116 | +    # Make sure they are sorted, and take the last one
 | |
| 117 | +    sorted_versions = sorted(filtered_versions)
 | |
| 118 | +    latest_version = sorted_versions[-1]
 | |
| 119 | + | |
| 120 | +    return latest_version
 | |
| 121 | + | |
| 122 | + | |
| 123 | +@click.command(short_help="Generate the version badges")
 | |
| 124 | +@click.option('--release', is_flag=True, default=False,
 | |
| 125 | +              help="Whether to generate the badge for the release version")
 | |
| 126 | +def generate_badges(release):
 | |
| 127 | +    """Generate the version badge svg files
 | |
| 128 | +    """
 | |
| 129 | +    major, minor, micro = guess_version(release)
 | |
| 130 | + | |
| 131 | +    if release:
 | |
| 132 | +        badge_name = 'release'
 | |
| 133 | +        color = RELEASE_COLOR
 | |
| 134 | +    else:
 | |
| 135 | +        badge_name = 'snapshot'
 | |
| 136 | +        color = SNAPSHOT_COLOR
 | |
| 137 | + | |
| 138 | +    brief_version = '{major}.{minor}'.format(major=major, minor=minor)
 | |
| 139 | +    full_version = '{major}.{minor}.{micro}'.format(major=major, minor=minor, micro=micro)
 | |
| 140 | +    url_target = URL_FORMAT.format(brief_version=brief_version, full_version=full_version)
 | |
| 141 | +    badge = BADGE_TEMPLATE.format(badge_name=badge_name,
 | |
| 142 | +                                  version=full_version,
 | |
| 143 | +                                  color=color,
 | |
| 144 | +                                  url_target=url_target)
 | |
| 145 | +    click.echo(badge, nl=False)
 | |
| 146 | +    return 0
 | |
| 147 | + | |
| 148 | + | |
| 149 | +if __name__ == '__main__':
 | |
| 150 | +    generate_badges() | 
| ... | ... | @@ -96,8 +96,8 @@ def _ansi2html_get_styles(palette): | 
| 96 | 96 |  | 
| 97 | 97 |          for g in range(24):
 | 
| 98 | 98 |              i = g + 232
 | 
| 99 | -            l = g * 10 + 8
 | |
| 100 | -            indexed_style['%s' % i] = ''.join('%02X' % c if 0 <= c <= 255 else None for c in (l, l, l))
 | |
| 99 | +            L = g * 10 + 8
 | |
| 100 | +            indexed_style['%s' % i] = ''.join('%02X' % c if 0 <= c <= 255 else None for c in (L, L, L))
 | |
| 101 | 101 |  | 
| 102 | 102 |          _ANSI2HTML_STYLES[palette] = (regular_style, bold_style, indexed_style)
 | 
| 103 | 103 |      return _ANSI2HTML_STYLES[palette]
 | 
| ... | ... | @@ -455,6 +455,7 @@ def run_bst(directory, force, source_cache, description, palette): | 
| 455 | 455 |  | 
| 456 | 456 |      return 0
 | 
| 457 | 457 |  | 
| 458 | + | |
| 458 | 459 |  if __name__ == '__main__':
 | 
| 459 | 460 |      try:
 | 
| 460 | 461 |          run_bst()
 | 
| ... | ... | @@ -19,10 +19,10 @@ | 
| 19 | 19 |  #
 | 
| 20 | 20 |  import os
 | 
| 21 | 21 |  import sys
 | 
| 22 | -sys.path.insert(0, os.path.abspath('..'))
 | |
| 23 | - | |
| 24 | 22 |  from buildstream import __version__
 | 
| 25 | 23 |  | 
| 24 | +sys.path.insert(0, os.path.abspath('..'))
 | |
| 25 | + | |
| 26 | 26 |  # -- General configuration ------------------------------------------------
 | 
| 27 | 27 |  | 
| 28 | 28 |  # If your documentation needs a minimal Sphinx version, state it here.
 | 
| ... | ... | @@ -112,7 +112,7 @@ add_module_names = False | 
| 112 | 112 |  pygments_style = 'sphinx'
 | 
| 113 | 113 |  | 
| 114 | 114 |  # A list of ignored prefixes for module index sorting.
 | 
| 115 | -modindex_common_prefix = [ 'buildstream.' ]
 | |
| 115 | +modindex_common_prefix = ['buildstream.']
 | |
| 116 | 116 |  | 
| 117 | 117 |  # If true, keep warnings as "system message" paragraphs in the built documents.
 | 
| 118 | 118 |  # keep_warnings = False
 | 
| ... | ... | @@ -160,7 +160,7 @@ html_theme = 'sphinx_rtd_theme' | 
| 160 | 160 |  # Add any paths that contain custom static files (such as style sheets) here,
 | 
| 161 | 161 |  # relative to this directory. They are copied after the builtin static files,
 | 
| 162 | 162 |  # so a file named "default.css" will overwrite the builtin "default.css".
 | 
| 163 | -html_static_path = []
 | |
| 163 | +html_static_path = ['badges']
 | |
| 164 | 164 |  | 
| 165 | 165 |  # Add any extra paths that contain custom files (such as robots.txt or
 | 
| 166 | 166 |  # .htaccess) here, relative to this directory. These files are copied
 | 
| ... | ... | @@ -13,8 +13,12 @@ For example, for a given version number ``X.Y.Z`` | 
| 13 | 13 |   * The ``X.<odd number>.*`` versions are development spanshots intended for testing.
 | 
| 14 | 14 |  | 
| 15 | 15 |  If you are :ref:`installing from git <install_git_checkout>`, please look for the latest
 | 
| 16 | -tag in the latest release branch to ensure you're getting the latest release.
 | |
| 16 | +tag to ensure you're getting the latest release.
 | |
| 17 | 17 |  | 
| 18 | -Current release branches:
 | |
| 19 | - * `bst-1.2 (latest) <https://gitlab.com/BuildStream/buildstream/commits/bst-1.2>`_
 | |
| 20 | - * `bst-1.0 (deprecated) <https://gitlab.com/BuildStream/buildstream/commits/bst-1.0>`_ | |
| 18 | +* Latest release:
 | |
| 19 | + | |
| 20 | +  .. include:: release-badge.rst
 | |
| 21 | + | |
| 22 | +* Latest development snapshot:
 | |
| 23 | + | |
| 24 | +  .. include:: snapshot-badge.rst | 
| ... | ... | @@ -4,6 +4,11 @@ | 
| 4 | 4 |  | 
| 5 | 5 |  Install
 | 
| 6 | 6 |  =======
 | 
| 7 | + | |
| 8 | +.. include:: release-badge.rst
 | |
| 9 | + | |
| 10 | +.. include:: snapshot-badge.rst
 | |
| 11 | + | |
| 7 | 12 |  This section provides instructions for installing BuildStream and its
 | 
| 8 | 13 |  companion artifact server on various platforms, along with any installation
 | 
| 9 | 14 |  related materials.
 | 
| 1 | + | |
| 2 | +.. Use this file to include the badge in the documentation, but not in
 | |
| 3 | +   the README.rst or gitlab rendered materials, that doesnt work.
 | |
| 4 | + | |
| 5 | +   This is partly a workaround for a sphinx issue, we will be able
 | |
| 6 | +   to avoid the raw html once this is implemented in sphinx:
 | |
| 7 | + | |
| 8 | +       https://github.com/sphinx-doc/sphinx/issues/2240
 | |
| 9 | + | |
| 10 | +   Using the <object> tag instead of the <img> tag which sphinx generates
 | |
| 11 | +   allows the svg to be "interactive", for us this basically means that
 | |
| 12 | +   the link we encode in the badge svg is used, rather than static urls
 | |
| 13 | +   which need to be used around the <img> tag.
 | |
| 14 | + | |
| 15 | +   WARNING: The custom CSS on the style tag will need to change if we
 | |
| 16 | +            change the theme, so that the <object> tag behaves similar
 | |
| 17 | +	    to how the <img> tag is themed by the style sheets.
 | |
| 18 | + | |
| 19 | +.. raw:: html
 | |
| 20 | + | |
| 21 | +   <a class="reference external image-reference">
 | |
| 22 | +     <object style="margin-bottom:24px;vertical-align:middle"
 | |
| 23 | +             data=""
 | |
| 24 | +	     type="image/svg+xml"/>
 | |
| 25 | +     </object>
 | |
| 26 | +   </a> | 
| 1 | + | |
| 2 | +.. Use this file to include the badge in the documentation, but not in
 | |
| 3 | +   the README.rst or gitlab rendered materials, that doesnt work.
 | |
| 4 | + | |
| 5 | +   This is partly a workaround for a sphinx issue, we will be able
 | |
| 6 | +   to avoid the raw html once this is implemented in sphinx:
 | |
| 7 | + | |
| 8 | +       https://github.com/sphinx-doc/sphinx/issues/2240
 | |
| 9 | + | |
| 10 | +   Using the <object> tag instead of the <img> tag which sphinx generates
 | |
| 11 | +   allows the svg to be "interactive", for us this basically means that
 | |
| 12 | +   the link we encode in the badge svg is used, rather than static urls
 | |
| 13 | +   which need to be used around the <img> tag.
 | |
| 14 | + | |
| 15 | +   WARNING: The custom CSS on the style tag will need to change if we
 | |
| 16 | +            change the theme, so that the <object> tag behaves similar
 | |
| 17 | +	    to how the <img> tag is themed by the style sheets.
 | |
| 18 | + | |
| 19 | +.. raw:: html
 | |
| 20 | + | |
| 21 | +   <a class="reference external image-reference">
 | |
| 22 | +     <object style="margin-bottom:24px;vertical-align:middle"
 | |
| 23 | +             data=""
 | |
| 24 | +	     type="image/svg+xml"/>
 | |
| 25 | +     </object>
 | |
| 26 | +   </a> | 
| ... | ... | @@ -25,7 +25,14 @@ import subprocess | 
| 25 | 25 |  import sys
 | 
| 26 | 26 |  import versioneer
 | 
| 27 | 27 |  | 
| 28 | -if sys.version_info[0] != 3 or sys.version_info[1] < 5:
 | |
| 28 | + | |
| 29 | +##################################################################
 | |
| 30 | +# Python requirements
 | |
| 31 | +##################################################################
 | |
| 32 | +REQUIRED_PYTHON_MAJOR = 3
 | |
| 33 | +REQUIRED_PYTHON_MINOR = 5
 | |
| 34 | + | |
| 35 | +if sys.version_info[0] != REQUIRED_PYTHON_MAJOR or sys.version_info[1] < REQUIRED_PYTHON_MINOR:
 | |
| 29 | 36 |      print("BuildStream requires Python >= 3.5")
 | 
| 30 | 37 |      sys.exit(1)
 | 
| 31 | 38 |  | 
| ... | ... | @@ -242,11 +249,28 @@ setup(name='BuildStream', | 
| 242 | 249 |  | 
| 243 | 250 |        author='BuildStream Developers',
 | 
| 244 | 251 |        author_email='buildstream-list gnome org',
 | 
| 252 | +      classifiers=[
 | |
| 253 | +          'Environment :: Console',
 | |
| 254 | +          'Intended Audience :: Developers',
 | |
| 255 | +          'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)',
 | |
| 256 | +          'Operating System :: POSIX',
 | |
| 257 | +          'Programming Language :: Python :: 3',
 | |
| 258 | +          'Programming Language :: Python :: 3.5',
 | |
| 259 | +          'Programming Language :: Python :: 3.6',
 | |
| 260 | +          'Programming Language :: Python :: 3.7',
 | |
| 261 | +          'Topic :: Software Development :: Build Tools'
 | |
| 262 | +      ],
 | |
| 245 | 263 |        description='A framework for modelling build pipelines in YAML',
 | 
| 246 | 264 |        license='LGPL',
 | 
| 247 | 265 |        long_description=long_description,
 | 
| 248 | 266 |        long_description_content_type='text/x-rst; charset=UTF-8',
 | 
| 249 | 267 |        url='https://gitlab.com/BuildStream/buildstream',
 | 
| 268 | +      project_urls={
 | |
| 269 | +          'Documentation': 'https://buildstream.gitlab.io/buildstream/',
 | |
| 270 | +          'Tracker': 'https://gitlab.com/BuildStream/buildstream/issues',
 | |
| 271 | +          'Mailing List': 'https://mail.gnome.org/mailman/listinfo/buildstream-list'
 | |
| 272 | +      },
 | |
| 273 | +      python_requires='~={}.{}'.format(REQUIRED_PYTHON_MAJOR, REQUIRED_PYTHON_MINOR),
 | |
| 250 | 274 |        packages=find_packages(exclude=('tests', 'tests.*')),
 | 
| 251 | 275 |        package_data={'buildstream': ['plugins/*/*.py', 'plugins/*/*.yaml',
 | 
| 252 | 276 |                                      'data/*.yaml', 'data/*.sh.in']},
 | 
| ... | ... | @@ -129,6 +129,7 @@ def assert_cache_keys(project_dir, output): | 
| 129 | 129 |                               "Use tests/cachekey/update.py to automatically " +
 | 
| 130 | 130 |                               "update this test case")
 | 
| 131 | 131 |  | 
| 132 | + | |
| 132 | 133 |  ##############################################
 | 
| 133 | 134 |  #             Test Entry Point               #
 | 
| 134 | 135 |  ##############################################
 | 
| ... | ... | @@ -65,5 +65,6 @@ def update_keys(): | 
| 65 | 65 |  | 
| 66 | 66 |                  write_expected_key(element_name, actual_keys[element_name])
 | 
| 67 | 67 |  | 
| 68 | + | |
| 68 | 69 |  if __name__ == '__main__':
 | 
| 69 | 70 |      update_keys() | 
| 1 | 1 |  import os
 | 
| 2 | 2 |  import pytest
 | 
| 3 | +import sys
 | |
| 3 | 4 |  from buildstream import _yaml
 | 
| 4 | 5 |  from buildstream._exceptions import ErrorDomain, LoadErrorReason
 | 
| 5 | 6 |  from tests.testutils.runcli import cli
 | 
| ... | ... | @@ -72,3 +73,20 @@ def test_missing_variable(cli, datafiles, tmpdir): | 
| 72 | 73 |      ])
 | 
| 73 | 74 |      result.assert_main_error(ErrorDomain.LOAD,
 | 
| 74 | 75 |                               LoadErrorReason.UNRESOLVED_VARIABLE)
 | 
| 76 | + | |
| 77 | +@pytest.mark.timeout(3, method="signal")
 | |
| 78 | +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'cyclic_variables'))
 | |
| 79 | +def test_cyclic_variables(cli, datafiles):
 | |
| 80 | +    print_warning("Performing cyclic test, if this test times out it will " +
 | |
| 81 | +                  "exit the test sequence")
 | |
| 82 | +    project = os.path.join(datafiles.dirname, datafiles.basename)
 | |
| 83 | +    result = cli.run(project=project, silent=True, args=[
 | |
| 84 | +        "build", "cyclic.bst"
 | |
| 85 | +    ])
 | |
| 86 | +    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.RECURSIVE_VARIABLE)
 | |
| 87 | + | |
| 88 | + | |
| 89 | +def print_warning(msg):
 | |
| 90 | +    RED, END = "\033[91m", "\033[0m"
 | |
| 91 | +    print(("\n{}{}{}").format(RED, msg, END), file=sys.stderr)
 | |
| 92 | + | 
| 1 | +kind: manual
 | |
| 2 | + | |
| 3 | +variables:
 | |
| 4 | +  a: "%{prefix}/a"
 | |
| 5 | +  prefix: "%{a}/some_prefix/" | |
| \ No newline at end of file | 
| 1 | +name: test | 
| ... | ... | @@ -288,6 +288,7 @@ def test_build_checkout_force_tarball(datafiles, cli): | 
| 288 | 288 |      assert os.path.join('.', 'usr', 'bin', 'hello') in tar.getnames()
 | 
| 289 | 289 |      assert os.path.join('.', 'usr', 'include', 'pony.h') in tar.getnames()
 | 
| 290 | 290 |  | 
| 291 | + | |
| 291 | 292 |  fetch_build_checkout_combos = \
 | 
| 292 | 293 |      [("strict", kind) for kind in ALL_REPO_KINDS] + \
 | 
| 293 | 294 |      [("non-strict", kind) for kind in ALL_REPO_KINDS]
 | 
| ... | ... | @@ -94,14 +94,28 @@ class Result(): | 
| 94 | 94 |      #    error_reason (any): The reason field of the error which occurred
 | 
| 95 | 95 |      #    fail_message (str): An optional message to override the automatic
 | 
| 96 | 96 |      #                        assertion error messages
 | 
| 97 | +    #    debug (bool): If true, prints information regarding the exit state of the result()
 | |
| 97 | 98 |      # Raises:
 | 
| 98 | 99 |      #    (AssertionError): If any of the assertions fail
 | 
| 99 | 100 |      #
 | 
| 100 | 101 |      def assert_main_error(self,
 | 
| 101 | 102 |                            error_domain,
 | 
| 102 | 103 |                            error_reason,
 | 
| 103 | -                          fail_message=''):
 | |
| 104 | - | |
| 104 | +                          fail_message='',
 | |
| 105 | +                          *, debug=False):
 | |
| 106 | +        if debug:
 | |
| 107 | +            print(
 | |
| 108 | +                """
 | |
| 109 | +                Exit code: {}
 | |
| 110 | +                Exception: {}
 | |
| 111 | +                Domain:    {}
 | |
| 112 | +                Reason:    {}
 | |
| 113 | +                """.format(
 | |
| 114 | +                    self.exit_code,
 | |
| 115 | +                    self.exception,
 | |
| 116 | +                    self.exception.domain,
 | |
| 117 | +                    self.exception.reason
 | |
| 118 | +                ))
 | |
| 105 | 119 |          assert self.exit_code == -1, fail_message
 | 
| 106 | 120 |          assert self.exc is not None, fail_message
 | 
| 107 | 121 |          assert self.exception is not None, fail_message
 | 
