Skip to content

Commit 59042d4

Browse files
joshua-zivkovicjuergbiJoshuaZivkovic
committed
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 <[email protected]> Co-authored-by: Joshua Zivkovic <[email protected]>
1 parent 4a04dac commit 59042d4

File tree

6 files changed

+72
-79
lines changed

6 files changed

+72
-79
lines changed

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: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@
8585
from . import utils
8686
from . import _cachekey
8787
from . import _site
88-
from .node import Node
88+
from .node import Node, MappingNode
8989
from .plugin import Plugin
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
@@ -102,7 +102,7 @@
102102

103103
if TYPE_CHECKING:
104104
from typing import Tuple
105-
from .node import MappingNode, ScalarNode, SequenceNode
105+
from .node import ScalarNode, SequenceNode
106106
from .types import SourceRef
107107

108108
# pylint: disable=cyclic-import
@@ -2635,19 +2635,27 @@ 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+
defined_attrs = project._project_conf.get_mapping("source-provenance-fields", None) or project.source_provenance_fields
2642+
try:
2643+
provenance_node.validate_keys(defined_attrs.keys())
2644+
except LoadError as E:
2645+
raise LoadError(
2646+
"Specified source attribute not defined in project config\n {}".format(E),
2647+
LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE)
2648+
2649+
# make sure everything is a string
2650+
provenance_node = MappingNode.from_dict({key: value.as_str() for key, value in provenance_node.items()})
26432651

26442652
meta_source = MetaSource(
26452653
self.name,
26462654
index,
26472655
self.get_kind(),
26482656
kind.as_str(),
26492657
directory,
2650-
provenance,
2658+
provenance_node,
26512659
source,
26522660
load_element.first_pass,
26532661
)

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: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,11 @@
3838
to provide additional source provenance related metadata which will later
3939
be reported in :class:`.SourceInfo` objects.
4040
41-
The ``provenance`` dictionary supports the following fields:
41+
The ``provenance`` dictionary itself does not have any specific required keys.
4242
43-
* Homepage
44-
45-
The ``homepage`` attribute can be used to specify the project homepage URL
46-
47-
* Issue Tracker
48-
49-
The ``issue-tracker`` attribute can be used to specify the project's issue tracking URL
43+
Any attribute used in the ``provenance`` dictionary of a source must be
44+
defined in the project.conf using the ``source-provenance-fields`` dictionary
45+
to define the attribute and its significance.
5046
5147
*Since: 2.5*
5248
@@ -378,9 +374,9 @@
378374
from .node import MappingNode
379375
from .plugin import Plugin
380376
from .sourcemirror import SourceMirror
381-
from .types import SourceRef, CoreWarnings, FastEnum, _SourceProvenance
382-
from ._exceptions import BstError, ImplError, PluginError
383-
from .exceptions import ErrorDomain
377+
from .types import SourceRef, CoreWarnings, FastEnum
378+
from ._exceptions import BstError, ImplError, PluginError, LoadError
379+
from .exceptions import ErrorDomain, LoadErrorReason
384380
from ._loader.metasource import MetaSource
385381
from ._projectrefs import ProjectRefStorage
386382
from ._cachekey import generate_key
@@ -396,6 +392,8 @@
396392

397393
# pylint: enable=cyclic-import
398394

395+
SourceProvenance = MappingNode
396+
399397

400398
class SourceError(BstError):
401399
"""This exception should be raised by :class:`.Source` implementations
@@ -553,8 +551,7 @@ def __init__(
553551
self,
554552
kind: str,
555553
url: str,
556-
homepage: Optional[str],
557-
issue_tracker: Optional[str],
554+
provenance: Optional[SourceProvenance],
558555
medium: Union[SourceInfoMedium, str],
559556
version_type: Union[SourceVersionType, str],
560557
version: str,
@@ -572,14 +569,9 @@ def __init__(
572569
The url of the source input
573570
"""
574571

575-
self.homepage: Optional[str] = homepage
572+
self.provenance = provenance
576573
"""
577-
The project homepage URL
578-
"""
579-
580-
self.issue_tracker: Optional[str] = issue_tracker
581-
"""
582-
The project issue tracking URL
574+
The optional YAML node with source provenance attributes
583575
"""
584576

585577
self.medium: Union[SourceInfoMedium, str] = medium
@@ -642,10 +634,14 @@ def serialize(self) -> Dict[str, Union[str, Dict[str, str]]]:
642634
"url": self.url,
643635
}
644636

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
637+
if self.provenance is not None:
638+
# need to keep homepage/issue-tracker [also] at the top-level for backward compat
639+
if (homepage := self.provenance.get_str("homepage", None)) is not None:
640+
version_info["homepage"] = homepage
641+
if (issue_tracker := self.provenance.get_str("issue-tracker", None)) is not None:
642+
version_info["issue-tracker"] = issue_tracker
643+
644+
version_info["provenance"] = self.provenance
649645

650646
version_info["medium"] = medium_str
651647
version_info["version-type"] = version_type_str
@@ -824,9 +820,9 @@ def __init__(
824820
self.__element_kind = meta.element_kind # The kind of the element owning this source
825821
self._directory = meta.directory # Staging relative directory
826822
self.__variables = variables # The variables used to resolve the source's config
827-
self.__provenance: Optional[
828-
_SourceProvenance
829-
] = meta.provenance # The _SourceProvenance for general user provided SourceInfo
823+
self.__provenance: Optional[SourceProvenance] = (
824+
meta.provenance
825+
) # The source provenance for general user provided SourceInfo
830826

831827
self.__key = None # Cache key for source
832828

@@ -1393,23 +1389,33 @@ def create_source_info(
13931389
13941390
*Since: 2.5*
13951391
"""
1396-
homepage = None
1397-
issue_tracker = None
1392+
project = self._get_project()
13981393

13991394
if provenance_node is not None:
1400-
provenance: Optional[_SourceProvenance] = _SourceProvenance.new_from_node(provenance_node)
1395+
# Ensure provenance node keys are valid and values are all strings
1396+
defined_provenance_fields = (
1397+
project._project_conf.get_mapping("source-provenance-fields", None)
1398+
or project.source_provenance_fields
1399+
)
1400+
1401+
try:
1402+
provenance_node.validate_keys(defined_provenance_fields.keys())
1403+
except LoadError as E:
1404+
raise LoadError(
1405+
"Specified source attribute not defined in project config\n {}".format(E),
1406+
LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE,
1407+
)
1408+
1409+
# Make sure everything is a string
1410+
provenance = MappingNode.from_dict({key: value.as_str() for key, value in provenance_node.items()})
1411+
14011412
else:
14021413
provenance = self.__provenance
14031414

1404-
if provenance is not None:
1405-
homepage = provenance.homepage
1406-
issue_tracker = provenance.issue_tracker
1407-
14081415
return SourceInfo(
14091416
self.get_kind(),
14101417
url,
1411-
homepage,
1412-
issue_tracker,
1418+
provenance,
14131419
medium,
14141420
version_type,
14151421
version,

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)