Skip to content

Commit d18f672

Browse files
juergbiJoshuaZivkovic
authored andcommitted
Make source provenance generic
Remove harcoded SPDX attributes and make them be generic instead. Project allowed attributes are configured via the project config, these supported values a determined by buildstream-sbom's support Co-authored-by: Jürg Billeter <j@bitron.ch> Co-authored-by: Joshua Zivkovic <joshuazivkovic@codethink.co.uk>
1 parent bbc05ba commit d18f672

6 files changed

Lines changed: 91 additions & 72 deletions

File tree

src/buildstream/_project.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def __init__(
125125
self.sandbox: Optional[MappingNode] = None
126126
self.splits: Optional[MappingNode] = None
127127

128+
self.source_provenance_fields: Optional[MappingNode] = None # Source provenance fields and their description
129+
128130
#
129131
# Private members
130132
#
@@ -726,6 +728,7 @@ def _validate_toplevel_node(self, node, *, first_pass=False):
726728
"sources",
727729
"source-caches",
728730
"junctions",
731+
"source-provenance-fields",
729732
"(@)",
730733
"(?)",
731734
]
@@ -1005,6 +1008,7 @@ def _load_second_pass(self):
10051008
mount = _HostMount(path, host_path, optional)
10061009

10071010
self._shell_host_files.append(mount)
1011+
self.source_provenance_fields = config.get_mapping("source-provenance-fields")
10081012

10091013
# _load_pass():
10101014
#

src/buildstream/data/projectconfig.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ shell:
175175
#
176176
command: [ 'sh', '-i' ]
177177

178+
# Define the set of fields accepted in `provenance` dictionaries of sources.
179+
#
180+
source-provenance-fields:
181+
homepage: "The project homepage URL"
182+
issue-tracker: "The project's issue tracking URL"
183+
178184
# Defaults for bst commands
179185
#
180186
defaults:

src/buildstream/element.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
from .sandbox import _SandboxFlags, SandboxCommandError
9191
from .sandbox._config import SandboxConfig
9292
from .sandbox._sandboxremote import SandboxRemote
93-
from .types import _Scope, _CacheBuildTrees, _KeyStrength, OverlapAction, _DisplayKey, _SourceProvenance
93+
from .types import _Scope, _CacheBuildTrees, _KeyStrength, OverlapAction, _DisplayKey
9494
from ._artifact import Artifact
9595
from ._elementproxy import ElementProxy
9696
from ._elementsources import ElementSources
@@ -2635,19 +2635,18 @@ def __load_sources(self, load_element):
26352635
del source[Symbol.DIRECTORY]
26362636

26372637
# Provenance is optional
2638-
provenance_node = source.get_mapping(Symbol.PROVENANCE, default=None)
2639-
provenance = None
2638+
provenance_node: MappingNode = source.get_mapping(Symbol.PROVENANCE, default=None)
26402639
if provenance_node:
26412640
del source[Symbol.PROVENANCE]
2642-
provenance = _SourceProvenance.new_from_node(provenance_node)
2641+
provenance_node.validate_keys(project.source_provenance_fields.keys())
26432642

26442643
meta_source = MetaSource(
26452644
self.name,
26462645
index,
26472646
self.get_kind(),
26482647
kind.as_str(),
26492648
directory,
2650-
provenance,
2649+
provenance_node,
26512650
source,
26522651
load_element.first_pass,
26532652
)

src/buildstream/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,8 @@ class LoadErrorReason(Enum):
155155
This warning will be produced when a filename for a target contains invalid
156156
characters in its name.
157157
"""
158+
159+
UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE = 29
160+
"""
161+
Thee source provenance attribute specified was not defined in the project config
162+
"""

src/buildstream/source.py

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
4949
The ``issue-tracker`` attribute can be used to specify the project's issue tracking URL
5050
51+
TODO: document that fields can be configured in project
52+
5153
*Since: 2.5*
5254
5355
@@ -370,6 +372,7 @@
370372
"""
371373

372374
import os
375+
from collections import ChainMap
373376
from contextlib import contextmanager
374377
from typing import Iterable, Iterator, Optional, Tuple, Dict, Any, Set, TYPE_CHECKING, Union
375378
from dataclasses import dataclass
@@ -378,9 +381,9 @@
378381
from .node import MappingNode
379382
from .plugin import Plugin
380383
from .sourcemirror import SourceMirror
381-
from .types import SourceRef, CoreWarnings, FastEnum, _SourceProvenance
382-
from ._exceptions import BstError, ImplError, PluginError
383-
from .exceptions import ErrorDomain
384+
from .types import SourceRef, CoreWarnings, FastEnum
385+
from ._exceptions import BstError, ImplError, PluginError, LoadError
386+
from .exceptions import ErrorDomain, LoadErrorReason
384387
from ._loader.metasource import MetaSource
385388
from ._projectrefs import ProjectRefStorage
386389
from ._cachekey import generate_key
@@ -396,6 +399,7 @@
396399

397400
# pylint: enable=cyclic-import
398401

402+
SourceProvenance = MappingNode
399403

400404
class SourceError(BstError):
401405
"""This exception should be raised by :class:`.Source` implementations
@@ -553,8 +557,7 @@ def __init__(
553557
self,
554558
kind: str,
555559
url: str,
556-
homepage: Optional[str],
557-
issue_tracker: Optional[str],
560+
provenance: Optional[MappingNode],
558561
medium: Union[SourceInfoMedium, str],
559562
version_type: Union[SourceVersionType, str],
560563
version: str,
@@ -572,14 +575,9 @@ def __init__(
572575
The url of the source input
573576
"""
574577

575-
self.homepage: Optional[str] = homepage
576-
"""
577-
The project homepage URL
578-
"""
579-
580-
self.issue_tracker: Optional[str] = issue_tracker
578+
self.provenance = provenance
581579
"""
582-
The project issue tracking URL
580+
The optional YAML node with source provenance attributes
583581
"""
584582

585583
self.medium: Union[SourceInfoMedium, str] = medium
@@ -642,10 +640,14 @@ def serialize(self) -> Dict[str, Union[str, Dict[str, str]]]:
642640
"url": self.url,
643641
}
644642

645-
if self.homepage is not None:
646-
version_info["homepage"] = self.homepage
647-
if self.issue_tracker is not None:
648-
version_info["issue-tracker"] = self.issue_tracker
643+
if self.provenance is not None:
644+
# need to keep homepage/issue-tracker [also] at the top-level for backward compat
645+
if (homepage := self.provenance.get_str("homepage", None)) is not None:
646+
version_info["homepage"] = homepage
647+
if (issue_tracker := self.provenance.get_str("issue-tracker", None)) is not None:
648+
version_info["issue-tracker"] = issue_tracker
649+
650+
version_info["provenance"] = self.provenance
649651

650652
version_info["medium"] = medium_str
651653
version_info["version-type"] = version_type_str
@@ -825,7 +827,7 @@ def __init__(
825827
self._directory = meta.directory # Staging relative directory
826828
self.__variables = variables # The variables used to resolve the source's config
827829
self.__provenance: Optional[
828-
_SourceProvenance
830+
MappingNode
829831
] = meta.provenance # The _SourceProvenance for general user provided SourceInfo
830832

831833
self.__key = None # Cache key for source
@@ -927,7 +929,16 @@ def set_ref(self, ref, node):
927929
"""
928930
raise ImplError("Source plugin '{}' does not implement set_ref()".format(self.get_kind()))
929931

930-
def track(self, *, previous_sources_dir: Optional[str] = None) -> SourceRef:
932+
def load_source_provenance(self, node: MappingNode) -> None:
933+
raise ImplError("Source plugin '{}' does not implement load_source_provenance()".format(self.get_kind()))
934+
935+
def get_source_provenance(self) -> SourceProvenance:
936+
raise ImplError("Source plugin '{}' does not implement get_source_provenance()".format(self.get_kind()))
937+
938+
def set_source_provenance(self, source_provenance: SourceProvenance):
939+
raise ImplError("Source plugin '{}' does not implement set_source_provenance()".format(self.get_kind()))
940+
941+
def track(self, *, previous_sources_dir: Optional[str] = None) -> (SourceRef, SourceProvenance):
931942
"""Resolve a new ref from the plugin's track option
932943
933944
Args:
@@ -1393,23 +1404,24 @@ def create_source_info(
13931404
13941405
*Since: 2.5*
13951406
"""
1396-
homepage = None
1397-
issue_tracker = None
1407+
project = self._get_project()
13981408

13991409
if provenance_node is not None:
1400-
provenance: Optional[_SourceProvenance] = _SourceProvenance.new_from_node(provenance_node)
1410+
# TODO: Ensure provenance node keys are valid and values are all strings; same in element if we drop _SourceProvenance
1411+
try:
1412+
provenance_node.validate_keys(project.source_provenance_fields.keys())
1413+
except LoadError as E:
1414+
raise LoadError("Specified source attribute not defined in project config\n {}".format(E),
1415+
LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE)
1416+
1417+
provenance = provenance_node
14011418
else:
14021419
provenance = self.__provenance
14031420

1404-
if provenance is not None:
1405-
homepage = provenance.homepage
1406-
issue_tracker = provenance.issue_tracker
1407-
14081421
return SourceInfo(
14091422
self.get_kind(),
14101423
url,
1411-
homepage,
1412-
issue_tracker,
1424+
provenance,
14131425
medium,
14141426
version_type,
14151427
version,
@@ -1778,13 +1790,30 @@ def process_value(action, container, path, key, new_value):
17781790
# previous_sources_dir (str): directory where previous sources are staged
17791791
#
17801792
def _track(self, previous_sources_dir: Optional[str] = None) -> SourceRef:
1793+
def verify_provenance_attributes(provenance: SourceProvenance):
1794+
if type(provenance) is list:
1795+
# produce a list of unique attrs
1796+
unique_entries = {attr: "" for single_provenance in provenance for attr in single_provenance.keys()}
1797+
used_attrs = unique_entries.keys()
1798+
else:
1799+
used_attrs = provenance.keys()
1800+
1801+
undefined_attributes = list(set(used_attrs) - set(self._get_project().source_provenance_fields.keys()))
1802+
1803+
if len(undefined_attributes) > 0:
1804+
self.warn(f"Required source attributes not defined in project config: {undefined_attributes}")
1805+
17811806
if self.BST_REQUIRES_PREVIOUS_SOURCES_TRACK:
1782-
new_ref = self.__do_track(previous_sources_dir=previous_sources_dir)
1807+
new_ref, source_provenance = self.__do_track(previous_sources_dir=previous_sources_dir)
17831808
else:
1784-
new_ref = self.__do_track()
1809+
new_ref, source_provenance = self.__do_track()
17851810

17861811
current_ref = self.get_ref() # pylint: disable=assignment-from-no-return
17871812

1813+
# in the case of multi-source plugins, set_ref might update the provenance with the new content,
1814+
# designed to use ref, so grab the old content first
1815+
current_provenance: list[MappingNode] = self.get_source_provenance()
1816+
17881817
if new_ref is None:
17891818
# No tracking, keep current ref
17901819
new_ref = current_ref
@@ -1797,6 +1826,18 @@ def _track(self, previous_sources_dir: Optional[str] = None) -> SourceRef:
17971826

17981827
self._generate_key()
17991828

1829+
# need to account for multi-source provenance and process it appropriately
1830+
if source_provenance is not None:
1831+
verify_provenance_attributes(source_provenance)
1832+
1833+
if current_provenance is not None:
1834+
current_provenance = list(map(MappingNode.strip_node_info, current_provenance))
1835+
1836+
if current_provenance != source_provenance:
1837+
self.info(f"Updated source provenance content for {self.name}")
1838+
1839+
self.set_source_provenance(source_provenance)
1840+
18001841
return new_ref
18011842

18021843
# _requires_previous_sources()
@@ -2025,14 +2066,14 @@ def __do_track(self, **kwargs):
20252066
for mirror in reversed(project.get_alias_uris(alias, first_pass=self.__first_pass, tracking=True)):
20262067
new_source = self.__clone_for_uri(mirror)
20272068
try:
2028-
ref = new_source.track(**kwargs) # pylint: disable=assignment-from-none
2069+
ref, provenance = new_source.track(**kwargs) # pylint: disable=assignment-from-none
20292070
# FIXME: Need to consider temporary vs. permanent failures,
20302071
# and how this works with retries.
20312072
except BstError as e:
20322073
last_error = e
20332074
continue
20342075

2035-
return ref
2076+
return (ref, provenance)
20362077

20372078
raise last_error
20382079

src/buildstream/types.py

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -390,42 +390,6 @@ def new_from_node(cls, node: MappingNode) -> "_SourceMirror":
390390
return cls(name, aliases)
391391

392392

393-
# _SourceProvenance()
394-
#
395-
# A simple object describing user provided source provenance information
396-
#
397-
# Args:
398-
# homepage: The project homepage URL
399-
# issue_tracker: The project issue reporting URL
400-
#
401-
class _SourceProvenance:
402-
def __init__(self, homepage: Optional[str], issue_tracker: Optional[str]):
403-
self.homepage: Optional[str] = homepage
404-
self.issue_tracker: Optional[str] = issue_tracker
405-
406-
# new_from_node():
407-
#
408-
# Creates a _SourceProvenance() from a YAML loaded node.
409-
#
410-
# Args:
411-
# node: The configuration node describing the spec.
412-
#
413-
# Returns:
414-
# The described _SourceProvenance instance.
415-
#
416-
# Raises:
417-
# LoadError: If the node is malformed.
418-
#
419-
@classmethod
420-
def new_from_node(cls, node: MappingNode) -> "_SourceProvenance":
421-
node.validate_keys(["homepage", "issue-tracker"])
422-
423-
homepage: Optional[str] = node.get_str("homepage", None)
424-
issue_tracker: Optional[str] = node.get_str("issue-tracker", None)
425-
426-
return cls(homepage, issue_tracker)
427-
428-
429393
########################################
430394
# Type aliases #
431395
########################################

0 commit comments

Comments
 (0)