Skip to content

Commit 1f150fd

Browse files
Implement tracking for source provenance
Expand source tracking to also be able to work with source provenance data. This provides source plugins with the option of implementing source provenance data gathering whilst tracking source refs.
1 parent 920fb0f commit 1f150fd

File tree

4 files changed

+99
-9
lines changed

4 files changed

+99
-9
lines changed

src/buildstream/downloadablefilesource.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ class DownloadableFileSource(Source):
269269
def configure(self, node):
270270
self.original_url = node.get_str("url")
271271
self.ref = node.get_str("ref", None)
272+
self.source_provenance = node.get_mapping("provenance", None)
272273

273274
extra_data = {}
274275
self.url = self.translate_url(self.original_url, extra_data=extra_data)
@@ -310,6 +311,15 @@ def get_ref(self):
310311
def set_ref(self, ref, node):
311312
node["ref"] = self.ref = ref
312313

314+
def load_source_provenance(self, node):
315+
self.source_provenance = node.get_str("provenance", None)
316+
317+
def get_source_provenance(self):
318+
return self.source_provenance
319+
320+
def set_source_provenance(self, source_provenance, node):
321+
node["provenance"] = self.source_provenance = source_provenance
322+
313323
def track(self): # pylint: disable=arguments-differ
314324
# there is no 'track' field in the source to determine what/whether
315325
# or not to update refs, because tracking a ref is always a conscious
@@ -325,7 +335,7 @@ def track(self): # pylint: disable=arguments-differ
325335
)
326336
self.warn("Potential man-in-the-middle attack!", detail=detail)
327337

328-
return new_ref
338+
return new_ref, None
329339

330340
def fetch(self): # pylint: disable=arguments-differ
331341

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/plugins/sources/local.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ def get_ref(self):
9393
def set_ref(self, ref, node):
9494
pass # pragma: nocover
9595

96+
def load_source_provenance(self, node):
97+
pass
98+
99+
def get_source_provenance(self):
100+
return None
101+
102+
def set_source_provenance(self, source_provenance, node):
103+
pass
104+
96105
def fetch(self): # pylint: disable=arguments-differ
97106
# Nothing to do here for a local source
98107
pass # pragma: nocover

src/buildstream/source.py

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,8 @@
381381
from .plugin import Plugin
382382
from .sourcemirror import SourceMirror
383383
from .types import SourceRef, CoreWarnings, FastEnum
384-
from ._exceptions import BstError, ImplError, PluginError
385-
from .exceptions import ErrorDomain
384+
from ._exceptions import BstError, ImplError, PluginError, LoadError
385+
from .exceptions import ErrorDomain, LoadErrorReason
386386
from ._loader.metasource import MetaSource
387387
from ._projectrefs import ProjectRefStorage
388388
from ._cachekey import generate_key
@@ -929,7 +929,16 @@ def set_ref(self, ref, node):
929929
"""
930930
raise ImplError("Source plugin '{}' does not implement set_ref()".format(self.get_kind()))
931931

932-
def track(self, *, previous_sources_dir: Optional[str] = None) -> SourceRef:
932+
def load_source_provenance(self, node: MappingNode) -> None:
933+
pass
934+
935+
def get_source_provenance(self) -> SourceProvenance:
936+
return None
937+
938+
def set_source_provenance(self, source_provenance: SourceProvenance):
939+
pass
940+
941+
def track(self, *, previous_sources_dir: Optional[str] = None) -> SourceRef | tuple[SourceRef, SourceProvenance]:
933942
"""Resolve a new ref from the plugin's track option
934943
935944
Args:
@@ -954,7 +963,7 @@ def track(self, *, previous_sources_dir: Optional[str] = None) -> SourceRef:
954963
Working with the :ref:`source ref is discussed here <core_source_ref>`.
955964
"""
956965
# Allow a non implementation
957-
return None
966+
return None, None
958967

959968
def fetch(self, *, previous_sources_dir: Optional[str] = None) -> None:
960969
"""Fetch remote sources and mirror them locally, ensuring at least
@@ -1787,13 +1796,40 @@ def process_value(action, container, path, key, new_value):
17871796
# previous_sources_dir (str): directory where previous sources are staged
17881797
#
17891798
def _track(self, previous_sources_dir: Optional[str] = None) -> SourceRef:
1799+
def verify_provenance_attributes(provenance: SourceProvenance):
1800+
if type(provenance) is list:
1801+
# produce a list of unique attrs
1802+
unique_entries = {attr: "" for single_provenance in provenance for attr in single_provenance.keys()}
1803+
used_attrs = unique_entries.keys()
1804+
else:
1805+
used_attrs = provenance.keys()
1806+
1807+
project = self._get_project()
1808+
defined_provenance_fields = (
1809+
project._project_conf.get_mapping("source-provenance-fields", None) or project.source_provenance_fields
1810+
)
1811+
undefined_attributes = list(set(used_attrs) - set(defined_provenance_fields.keys()))
1812+
1813+
if len(undefined_attributes) > 0:
1814+
self.warn(f"Required source attributes not defined in project config: {undefined_attributes}")
1815+
17901816
if self.BST_REQUIRES_PREVIOUS_SOURCES_TRACK:
1791-
new_ref = self.__do_track(previous_sources_dir=previous_sources_dir)
1817+
r = self.__do_track(previous_sources_dir=previous_sources_dir)
1818+
else:
1819+
r = self.__do_track()
1820+
1821+
if type(r) is tuple:
1822+
new_ref, source_provenance = r
17921823
else:
1793-
new_ref = self.__do_track()
1824+
new_ref = r
1825+
source_provenance = None
17941826

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

1829+
# in the case of multi-source plugins, set_ref might update the provenance with the new content,
1830+
# designed to use ref, so grab the old content first
1831+
current_provenance: list[MappingNode] = self.get_source_provenance()
1832+
17971833
if new_ref is None:
17981834
# No tracking, keep current ref
17991835
new_ref = current_ref
@@ -1806,6 +1842,36 @@ def _track(self, previous_sources_dir: Optional[str] = None) -> SourceRef:
18061842

18071843
self._generate_key()
18081844

1845+
if source_provenance is not None:
1846+
verify_provenance_attributes(source_provenance)
1847+
1848+
if current_provenance is not None:
1849+
if type(current_provenance) is list:
1850+
current_provenance = list(map(MappingNode.strip_node_info, current_provenance))
1851+
else:
1852+
current_provenance = current_provenance.strip_node_info()
1853+
1854+
if current_provenance != source_provenance:
1855+
self.info(f"Updated source provenance content for {self.name}")
1856+
1857+
context = self._get_context()
1858+
project = self._get_project()
1859+
toplevel = context.get_toplevel_project()
1860+
toplevel_refs = self._project_refs(toplevel)
1861+
provenance = self._get_provenance()
1862+
1863+
element_name = self.__element_name
1864+
element_idx = self.__element_index
1865+
1866+
node = {}
1867+
if toplevel.ref_storage == ProjectRefStorage.PROJECT_REFS:
1868+
node = toplevel_refs.lookup_ref(project.name, element_name, element_idx, write=True)
1869+
1870+
if project is toplevel and not node:
1871+
node = provenance._node
1872+
1873+
self.set_source_provenance(source_provenance, node)
1874+
18091875
return new_ref
18101876

18111877
# _requires_previous_sources()
@@ -2034,14 +2100,14 @@ def __do_track(self, **kwargs):
20342100
for mirror in reversed(project.get_alias_uris(alias, first_pass=self.__first_pass, tracking=True)):
20352101
new_source = self.__clone_for_uri(mirror)
20362102
try:
2037-
ref = new_source.track(**kwargs) # pylint: disable=assignment-from-none
2103+
r = new_source.track(**kwargs) # pylint: disable=assignment-from-none
20382104
# FIXME: Need to consider temporary vs. permanent failures,
20392105
# and how this works with retries.
20402106
except BstError as e:
20412107
last_error = e
20422108
continue
20432109

2044-
return ref
2110+
return r
20452111

20462112
raise last_error
20472113

0 commit comments

Comments
 (0)