33import sys
44
55
6- def write (work_dir , tasks , config = None ):
6+ def write (work_dir , tasks , config = None , machine = None , baseline_dir = None ):
77 """
88 Write a file with provenance, such as the git version, conda packages,
99 command, and tasks, to the work directory
@@ -16,9 +16,15 @@ def write(work_dir, tasks, config=None):
1616 tasks : dict
1717 A dictionary describing all of the tasks and their steps
1818
19- config : polaris.config.PolarisConfigParser
19+ config : polaris.config.PolarisConfigParser, optional
2020 Configuration options for this task, a combination of user configs
2121 and the defaults for the machine and component
22+
23+ machine : str, optional
24+ The machine on which Polaris is being run
25+
26+ baseline_dir : str, optional
27+ The path to the baseline work directory, if any
2228 """
2329 polaris_git_version = None
2430 if os .path .exists ('.git' ):
@@ -69,6 +75,14 @@ def write(work_dir, tasks, config=None):
6975 f'component git version: { component_git_version } \n \n '
7076 )
7177 provenance_file .write (f'command: { calling_command } \n \n ' )
78+
79+ # Add readily parsable, PR-friendly metadata discovered at setup time
80+ _write_meta (provenance_file , 'machine' , machine )
81+ _write_scheduler_metadata (provenance_file , config )
82+ _write_meta (provenance_file , 'compiler' , _get_compiler (config ))
83+ _write_meta (provenance_file , 'work directory' , work_dir )
84+ _write_meta (provenance_file , 'build directory' , _get_build_dir (config ))
85+ _write_meta (provenance_file , 'baseline work directory' , baseline_dir )
7286 provenance_file .write ('tasks:\n ' )
7387
7488 for _ , task in tasks .items ():
@@ -126,3 +140,129 @@ def _get_component_git_version(config):
126140 os .chdir (cwd )
127141
128142 return component_git_version
143+
144+
145+ def _get_system (config ):
146+ if config is None :
147+ return None
148+ if config .has_option ('parallel' , 'system' ):
149+ return config .get ('parallel' , 'system' )
150+ return None
151+
152+
153+ def _resolve_scheduler_fields (config , system ):
154+ """Resolve partition/qos/constraint (slurm) or just constraint (pbs)."""
155+ partition = None
156+ qos = None
157+ constraint = None
158+
159+ if config is None :
160+ return partition , qos , constraint
161+
162+ if system == 'pbs' :
163+ # PBS: only constraint is relevant for our purposes
164+ cons_val = (
165+ config .get ('job' , 'constraint' )
166+ if config .has_option ('job' , 'constraint' )
167+ else None
168+ )
169+ if cons_val and cons_val != '<<<default>>>' :
170+ constraint = cons_val
171+ elif config .has_option ('parallel' , 'constraints' ):
172+ cons_list = config .getlist ('parallel' , 'constraints' )
173+ if cons_list :
174+ constraint = cons_list [0 ]
175+ return partition , qos , constraint
176+
177+ # Default to slurm-like resolution
178+ part_val = (
179+ config .get ('job' , 'partition' )
180+ if config .has_option ('job' , 'partition' )
181+ else None
182+ )
183+ if part_val and part_val != '<<<default>>>' :
184+ partition = part_val
185+ elif config .has_option ('parallel' , 'partitions' ):
186+ parts = config .getlist ('parallel' , 'partitions' )
187+ if parts :
188+ partition = parts [0 ]
189+
190+ qos_val = (
191+ config .get ('job' , 'qos' ) if config .has_option ('job' , 'qos' ) else None
192+ )
193+ if qos_val and qos_val != '<<<default>>>' :
194+ qos = qos_val
195+ elif config .has_option ('parallel' , 'qos' ):
196+ qos_list = config .getlist ('parallel' , 'qos' )
197+ if qos_list :
198+ qos = qos_list [0 ]
199+
200+ cons_val = (
201+ config .get ('job' , 'constraint' )
202+ if config .has_option ('job' , 'constraint' )
203+ else None
204+ )
205+ if cons_val and cons_val != '<<<default>>>' :
206+ constraint = cons_val
207+ elif config .has_option ('parallel' , 'constraints' ):
208+ cons_list = config .getlist ('parallel' , 'constraints' )
209+ if cons_list :
210+ constraint = cons_list [0 ]
211+
212+ return partition , qos , constraint
213+
214+
215+ def _get_compiler (config ):
216+ if config is None :
217+ return None
218+ if config .has_option ('deploy' , 'compiler' ):
219+ val = config .get ('deploy' , 'compiler' )
220+ return val or None
221+ return None
222+
223+
224+ def _get_build_dir (config ):
225+ if config is None :
226+ return None
227+ if config .has_option ('paths' , 'component_path' ):
228+ val = config .get ('paths' , 'component_path' )
229+ return val or None
230+ return None
231+
232+
233+ def _write_meta (provenance_file , label , value ):
234+ """Write a simple 'label: value' line if value is provided."""
235+ if value is None :
236+ return
237+ if isinstance (value , str ) and value .strip () == '' :
238+ return
239+ provenance_file .write (f'{ label } : { value } \n \n ' )
240+
241+
242+ def _write_scheduler_metadata (provenance_file , config ):
243+ """Write partition/qos/constraint metadata when available."""
244+ system = _get_system (config )
245+ partition , qos , constraint = _resolve_scheduler_fields (config , system )
246+ _write_meta (provenance_file , 'partition' , partition )
247+ _write_meta (provenance_file , 'qos' , qos )
248+ _write_meta (provenance_file , 'constraint' , constraint )
249+ if config .has_option ('paths' , 'component_path' ):
250+ component_path = config .get ('paths' , 'component_path' )
251+ else :
252+ component_path = None
253+
254+ if component_path is None or not os .path .exists (component_path ):
255+ return None
256+
257+ cwd = os .getcwd ()
258+ os .chdir (component_path )
259+
260+ try :
261+ args = ['git' , 'describe' , '--tags' , '--dirty' , '--always' ]
262+ component_git_version = subprocess .check_output (args ).decode ('utf-8' )
263+ component_git_version = component_git_version .strip ('\n ' )
264+ except subprocess .CalledProcessError :
265+ component_git_version = None
266+ os .chdir (cwd )
267+
268+ return component_git_version
0 commit comments