381381from .plugin import Plugin
382382from .sourcemirror import SourceMirror
383383from .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
386386from ._loader .metasource import MetaSource
387387from ._projectrefs import ProjectRefStorage
388388from ._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