Skip to content

Commit 182c33a

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 <j@bitron.ch> Co-authored-by: Joshua Zivkovic <joshuazivkovic@codethink.co.uk>
1 parent 4a04dac commit 182c33a

6 files changed

Lines changed: 109 additions & 86 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: 26 additions & 10 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,33 @@ 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 = (
2642+
project._project_conf.get_mapping("source-provenance-fields", None)
2643+
or project.source_provenance_fields
2644+
)
2645+
try:
2646+
provenance_node.validate_keys(defined_attrs.keys())
2647+
except LoadError as E:
2648+
raise LoadError(
2649+
"Specified source attribute not defined in project config\n {}".format(E),
2650+
LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE,
2651+
)
2652+
2653+
# make sure everything is a string
2654+
provenance_node = MappingNode.from_dict(
2655+
{key: value.as_str() for key, value in provenance_node.items()}
2656+
)
26432657

26442658
meta_source = MetaSource(
26452659
self.name,
26462660
index,
26472661
self.get_kind(),
26482662
kind.as_str(),
26492663
directory,
2650-
provenance,
2664+
provenance_node,
26512665
source,
26522666
load_element.first_pass,
26532667
)
@@ -3318,9 +3332,11 @@ def __update_cache_keys(self):
33183332
# encode the dependency's weak cache key instead of it's name.
33193333
#
33203334
dependencies = [
3321-
[e.project_name, e.name, e._get_cache_key(strength=_KeyStrength.WEAK)]
3322-
if self.BST_STRICT_REBUILD or e in self.__strict_dependencies
3323-
else [e.project_name, e.name]
3335+
(
3336+
[e.project_name, e.name, e._get_cache_key(strength=_KeyStrength.WEAK)]
3337+
if self.BST_STRICT_REBUILD or e in self.__strict_dependencies
3338+
else [e.project_name, e.name]
3339+
)
33243340
for e in self._dependencies(_Scope.BUILD)
33253341
]
33263342
self.__weak_cache_key = self._calculate_cache_key(dependencies)

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: 68 additions & 40 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
@@ -371,16 +367,26 @@
371367

372368
import os
373369
from contextlib import contextmanager
374-
from typing import Iterable, Iterator, Optional, Tuple, Dict, Any, Set, TYPE_CHECKING, Union
370+
from typing import (
371+
Iterable,
372+
Iterator,
373+
Optional,
374+
Tuple,
375+
Dict,
376+
Any,
377+
Set,
378+
TYPE_CHECKING,
379+
Union,
380+
)
375381
from dataclasses import dataclass
376382

377383
from . import _yaml, utils
378384
from .node import MappingNode
379385
from .plugin import Plugin
380386
from .sourcemirror import SourceMirror
381-
from .types import SourceRef, CoreWarnings, FastEnum, _SourceProvenance
382-
from ._exceptions import BstError, ImplError, PluginError
383-
from .exceptions import ErrorDomain
387+
from .types import SourceRef, CoreWarnings, FastEnum
388+
from ._exceptions import BstError, ImplError, PluginError, LoadError
389+
from .exceptions import ErrorDomain, LoadErrorReason
384390
from ._loader.metasource import MetaSource
385391
from ._projectrefs import ProjectRefStorage
386392
from ._cachekey import generate_key
@@ -396,6 +402,8 @@
396402

397403
# pylint: enable=cyclic-import
398404

405+
SourceProvenance = MappingNode
406+
399407

400408
class SourceError(BstError):
401409
"""This exception should be raised by :class:`.Source` implementations
@@ -409,9 +417,20 @@ class SourceError(BstError):
409417
"""
410418

411419
def __init__(
412-
self, message: str, *, detail: Optional[str] = None, reason: Optional[str] = None, temporary: bool = False
420+
self,
421+
message: str,
422+
*,
423+
detail: Optional[str] = None,
424+
reason: Optional[str] = None,
425+
temporary: bool = False,
413426
):
414-
super().__init__(message, detail=detail, domain=ErrorDomain.SOURCE, reason=reason, temporary=temporary)
427+
super().__init__(
428+
message,
429+
detail=detail,
430+
domain=ErrorDomain.SOURCE,
431+
reason=reason,
432+
temporary=temporary,
433+
)
415434

416435

417436
@dataclass
@@ -553,8 +572,7 @@ def __init__(
553572
self,
554573
kind: str,
555574
url: str,
556-
homepage: Optional[str],
557-
issue_tracker: Optional[str],
575+
provenance: Optional[SourceProvenance],
558576
medium: Union[SourceInfoMedium, str],
559577
version_type: Union[SourceVersionType, str],
560578
version: str,
@@ -572,14 +590,9 @@ def __init__(
572590
The url of the source input
573591
"""
574592

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

585598
self.medium: Union[SourceInfoMedium, str] = medium
@@ -642,10 +655,14 @@ def serialize(self) -> Dict[str, Union[str, Dict[str, str]]]:
642655
"url": self.url,
643656
}
644657

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
658+
if self.provenance is not None:
659+
# need to keep homepage/issue-tracker [also] at the top-level for backward compat
660+
if (homepage := self.provenance.get_str("homepage", None)) is not None:
661+
version_info["homepage"] = homepage
662+
if (issue_tracker := self.provenance.get_str("issue-tracker", None)) is not None:
663+
version_info["issue-tracker"] = issue_tracker
664+
665+
version_info["provenance"] = self.provenance.strip_node_info()
649666

650667
version_info["medium"] = medium_str
651668
version_info["version-type"] = version_type_str
@@ -824,9 +841,9 @@ def __init__(
824841
self.__element_kind = meta.element_kind # The kind of the element owning this source
825842
self._directory = meta.directory # Staging relative directory
826843
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
844+
self.__provenance: Optional[SourceProvenance] = (
845+
meta.provenance
846+
) # The source provenance for general user provided SourceInfo
830847

831848
self.__key = None # Cache key for source
832849

@@ -1393,23 +1410,33 @@ def create_source_info(
13931410
13941411
*Since: 2.5*
13951412
"""
1396-
homepage = None
1397-
issue_tracker = None
1413+
project = self._get_project()
13981414

1415+
provenance: SourceProvenance | None
13991416
if provenance_node is not None:
1400-
provenance: Optional[_SourceProvenance] = _SourceProvenance.new_from_node(provenance_node)
1417+
# Ensure provenance node keys are valid and values are all strings
1418+
defined_provenance_fields = (
1419+
project._project_conf.get_mapping("source-provenance-fields", None) or project.source_provenance_fields
1420+
)
1421+
1422+
try:
1423+
provenance_node.validate_keys(defined_provenance_fields.keys())
1424+
except LoadError as E:
1425+
raise LoadError(
1426+
"Specified source attribute not defined in project config\n {}".format(E),
1427+
LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE,
1428+
)
1429+
1430+
# Make sure everything is a string
1431+
provenance = MappingNode.from_dict({key: value.as_str() for key, value in provenance_node.items()})
1432+
14011433
else:
14021434
provenance = self.__provenance
14031435

1404-
if provenance is not None:
1405-
homepage = provenance.homepage
1406-
issue_tracker = provenance.issue_tracker
1407-
14081436
return SourceInfo(
14091437
self.get_kind(),
14101438
url,
1411-
homepage,
1412-
issue_tracker,
1439+
provenance,
14131440
medium,
14141441
version_type,
14151442
version,
@@ -1767,7 +1794,8 @@ def process_value(action, container, path, key, new_value):
17671794
_yaml.roundtrip_dump(data, filename)
17681795
except OSError as e:
17691796
raise SourceError(
1770-
"{}: Error saving source reference to '{}': {}".format(self, filename, e), reason="save-ref-error"
1797+
"{}: Error saving source reference to '{}': {}".format(self, filename, e),
1798+
reason="save-ref-error",
17711799
) from e
17721800

17731801
return True

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)