9 from shutil
import which
11 archBlock
= re
.compile( r
"(?:#[ ]*)(ARCH(?:.*\n)*?)(?:#{5,})", re
.I
)
12 kvPair
= re
.compile( r
"^(\w+)(?:[ \t]*=[ \t]*)(.*?)$", re
.I | re
.M
)
13 # Make this gnarly and complicated since configure.defaults has no standard formatting
14 # v start v OS V typical v MACOS
15 osAndArch
= re
.compile( r
"^ARCH[ ]+(\w+)[ ]+((?:\w+.*?),|(?:[(].*?[)]))", re
.I
)
16 # Just grab the first two words, thats what you get
17 osAndArchAlt
= re
.compile( r
"^ARCH[ ]+(\w+)[ ]+(\w+)", re
.I
)
19 referenceVar
= re
.compile( r
"[$]([(])?(\w+)(?(1)[)])", re
.I
)
20 compileObject
= re
.compile( r
"(\W|^)-c(\W|$)" )
21 configureRepl
= re
.compile( r
"(\W|^)CONFIGURE_\w+(\W|$)" )
25 def __init__( self
, lines
) :
29 self
.osArchLine_
= None
32 self
.crossPlatform_
= False
33 self
.skipCrossPlatform_
= True
34 self
.serialOpt_
= False
35 self
.smparOpt_
= False
36 self
.dmparOpt_
= False
40 self
.osArchLine_
= self
.lines_
.partition("\n")[0]
41 # First get os & archs
42 osarchMatch
= osAndArch
.match( self
.osArchLine_
)
44 if osarchMatch
is None :
45 osarchMatch
= osAndArchAlt
.match( self
.osArchLine_
)
46 if osarchMatch
is None :
47 print( "Could not find OS and architecture info in " + self
.osArchLine_
)
49 self
.os_
= osarchMatch
.group(1)
50 self
.archs_
= osarchMatch
.group(2).strip(",").split( " " )
52 if ( self
.os_
.lower() != platform
.system().lower() or
53 platform
.machine() not in self
.archs_
) :
54 self
.crossPlatform_
= True
56 # Allow cross platform or must not be cross platform
57 if not self
.skipCrossPlatform_
or ( self
.skipCrossPlatform_
and not self
.crossPlatform_
) :
59 # Find OpenMP/MPI compilation options
60 memOpts
= self
.osArchLine_
.partition( "#" )[-1].split( " " )
62 self
.serialOpt_
= "serial" in memOpts
63 self
.smparOpt_
= "smpar" in memOpts
64 self
.dmparOpt_
= "dmpar" in memOpts
65 self
.dmsmOpt_
= "dm+sm" in memOpts
67 for kvPairMatch
in kvPair
.finditer( self
.lines_
) :
68 self
.kvPairs_
[ kvPairMatch
.group(1) ] = kvPairMatch
.group(2)
69 self
.removeComments( kvPairMatch
.group(1) )
74 ######################################################################################################################
76 ## search and replace $(<var>) and $<var> instances
78 ######################################################################################################################
79 def dereference( self
, field
, fatal
=False ) :
80 # print( "Dereferencing " + field )
82 if field
in self
.kvPairs_
:
83 prevField
= self
.kvPairs_
[field
]
85 for refVarIter
in referenceVar
.finditer( prevField
) :
88 if refVarIter
is not None :
89 # Grab group 1 and check that it is in our kv pairs
90 refVar
= refVarIter
.group(2)
91 # print( "Found variable {0} in field {1}".format( refVar, field ) )
92 if refVar
not in self
.kvPairs_
:
93 # Try to use the environment variables
94 if refVar
in os
.environ
:
95 envSub
= os
.environ
[ refVar
]
98 # print( "Could not rereference : " + refVar )
104 # This is an environment variable
105 if envSub
is not None :
106 self
.kvPairs_
[field
] = self
.kvPairs_
[field
].replace(
107 "{var}".format( var
=refVarIter
.group(0) ),
110 # This is a kv pair, recurse
113 self
.dereference( refVar
, fatal
)
115 # Replace in original
116 self
.kvPairs_
[field
] = self
.kvPairs_
[field
].replace(
117 "{var}".format( var
=refVarIter
.group(0) ),
118 self
.kvPairs_
[refVar
]
121 def removeReferences( self
, field
, specifics
=[] ) :
122 if field
in self
.kvPairs_
:
124 for specific
in specifics
:
125 self
.kvPairs_
[ field
] = self
.kvPairs_
[ field
].replace(
126 "$({var})".format( var
=specific
),
130 self
.kvPairs_
[ field
] = referenceVar
.sub( "", self
.kvPairs_
[ field
] )
133 def removeComments( self
, field
) :
134 if field
in self
.kvPairs_
:
135 self
.kvPairs_
[ field
] = self
.kvPairs_
[ field
].split( "#", 1 )[0]
137 def splitIntoFieldAndFlags( self
, field
) :
138 # Fix flags being mixed with programs
139 if field
in self
.kvPairs_
:
140 fieldValue
= self
.kvPairs_
[ field
]
142 self
.kvPairs_
[field
] = fieldValue
.partition(" ")[0]
143 self
.kvPairs_
[field
+ "_FLAGS"] = fieldValue
.partition(" ")[1]
145 ######################################################################################################################
147 ## Clean up the stanza so kv pairs can be used as-is
149 ######################################################################################################################
150 def sanitize( self
) :
151 # Fix problematic variables
152 self
.dereference( "DM_FC" )
153 self
.dereference( "DM_CC" )
154 self
.removeReferences( "FCBASEOPTS_NO_G" )
155 # Get rid of all these mixed up flags, these are handled by cmake natively or
156 # just in the wrong place
157 self
.removeReferences( "FCBASEOPTS", [ "FCDEBUG", "FORMAT_FREE", "BYTESWAPIO", ] )
158 self
.removeReferences( "FFLAGS", [ "FORMAT_FREE", "FORMAT_FIXED" ] )
159 self
.removeReferences( "F77FLAGS", [ "FORMAT_FREE", "FORMAT_FIXED" ] )
161 self
.dereference( "FCBASEOPTS" )
163 # Remove rogue compile commands that should *NOT* even be here
164 for keyToSan
in self
.kvPairs_
.keys() :
165 self
.kvPairs_
[ keyToSan
] = configureRepl
.sub( r
"\1\2", self
.kvPairs_
[ keyToSan
] ).strip()
166 self
.kvPairs_
[ keyToSan
] = compileObject
.sub( r
"\1\2", self
.kvPairs_
[ keyToSan
] ).strip()
169 # Now fix certain ones that are mixing programs with flags all mashed into one option
170 self
.splitIntoFieldAndFlags( "SFC" )
171 self
.splitIntoFieldAndFlags( "SCC" )
172 self
.splitIntoFieldAndFlags( "DM_FC" )
173 self
.splitIntoFieldAndFlags( "DM_CC" )
174 self
.splitIntoFieldAndFlags( "CPP" )
175 self
.splitIntoFieldAndFlags( "M4" )
177 # Now deref all the rest
178 for key
in self
.kvPairs_
:
179 self
.dereference( key
)
180 # And for final measure strip
181 self
.kvPairs_
[ key
] = self
.kvPairs_
[ key
].strip()
183 def serialCompilersAvailable( self
) :
184 return which( self
.kvPairs_
["SFC"] ) is not None and which( self
.kvPairs_
["SCC"] ) is not None
186 def dmCompilersAvailable( self
) :
187 return which( self
.kvPairs_
["DM_FC"] ) is not None and which( self
.kvPairs_
["DM_CC"] ) is not None
189 ######################################################################################################################
191 ## string representation to view as option
193 ######################################################################################################################
195 # base = """OS {os:<8} ARCHITECTURES {archs:<20}
198 # >> CCOMP = {CCOMP:<12}
199 # >> DM_FC = {DM_FC:<12}
200 # >> DM_CC = {DM_CC:<12}
202 base
= """ {os:<10} {recSFC} {SFC:<11} / {recSCC} {SCC:<11} / {recDM_FC} {DM_FC:<11} / {recDM_CC} {DM_CC:<11}"""
203 text
= inspect
.cleandoc( base
).format(
205 recSFC
=( "!!" if which( self
.kvPairs_
["SFC"] ) is None else (" " * 2 ) ),
206 recSCC
=( "!!" if which( self
.kvPairs_
["SCC"] ) is None else (" " * 2 ) ),
207 recDM_FC
=( "!!" if which( self
.kvPairs_
["DM_FC"] ) is None else (" " * 2 ) ),
208 recDM_CC
=( "!!" if which( self
.kvPairs_
["DM_CC"] ) is None else (" " * 2 ) ),
209 # archs=str(self.archs_),
210 SFC
=str( self
.kvPairs_
["SFC"] ),
211 SCC
=str( self
.kvPairs_
["SCC"] ),
212 DM_FC
=str( self
.kvPairs_
["DM_FC"] ),
213 DM_CC
=str( self
.kvPairs_
["DM_CC"] )
215 # text += "\n" + "\n".join( [ "{key:<18} = {value}".format( key=key, value=value) for key, value in self.kvPairs_.items() ] )
218 ######################################################################################################################
220 ## Find first apparent difference between two stanzas
222 ######################################################################################################################
224 def findFirstDifference( rhStanza
, lhStanza
, maxLength
=32 ) :
245 for rhKey
, rhValue
in rhStanza
.kvPairs_
.items() :
246 if rhKey
in valuesToCheck
and rhKey
in lhStanza
.kvPairs_
:
247 # Qualifies for difference
248 if rhValue
!= lhStanza
.kvPairs_
[rhKey
] :
250 value
= "{key:<12} = {value}".format( key
=rhKey
, value
=lhStanza
.kvPairs_
[rhKey
] )
253 value
= ( value
[:maxLength
] + "..." ) if len( value
) > maxLength
else value
257 ########################################################################################################################
261 ########################################################################################################################
262 def getOptionsParser() :
263 parser
= argparse
.ArgumentParser( )
265 # https://stackoverflow.com/a/24181138
266 requiredNamed
= parser
.add_argument_group( "required named arguments" )
268 requiredNamed
.add_argument(
271 help="configure.defaults file holding all stanza configurations",
275 requiredNamed
.add_argument(
277 dest
="cmakeTemplateFile",
278 help="cmake template file for configuring stanza into cmake syntax",
282 requiredNamed
.add_argument(
284 dest
="outputConfigFile",
285 help="cmake output toolchain config file for selected stanza",
293 help="Use preselected stanza configuration, if multiple match grabs the first one",
299 "-x", "--skipCMakeOptions",
300 dest
="skipCMakeOptions",
301 help="Skip query of available CMake options",
308 dest
="sourceCMakeFile",
309 help="Required unless -x/--skipCMakeOptions set, project cmake source file used to determine available options",
317 class Options(object):
318 """Empty namespace"""
321 ########################################################################################################################
323 ## Select stanza to operate on
325 ########################################################################################################################
326 def selectStanza( options
) :
328 fp
= open( options
.configFile
, 'r' )
332 # Now grab the blocks and parse
334 # Gather all stanzas available
335 for stanzaBlock
in archBlock
.finditer( lines
) :
336 stanza
= Stanza( stanzaBlock
.group(1) )
339 if not stanza
.crossPlatform_
and stanza
.serialCompilersAvailable() and ( stanza
.dmCompilersAvailable() or ( stanza
.serialOpt_
or stanza
.smparOpt_
) ) :
340 if "DESCRIPTION" not in stanza
.kvPairs_
:
341 # Of course WPS configure.defaults is different than WRF so descriptions are embedded in the comments
342 stanza
.kvPairs_
[ "DESCRIPTION" ] = stanza
.osArchLine_
.partition( "," )[ -1 ].partition( "#" )[0].strip()
343 stanzas
.append( stanza
)
346 if options
.preselect
is None :
350 for stanza
in stanzas
:
351 stanzaConfig
= str( stanza
)
352 stanzaId
= "{idx:<3} ".format( idx
=stanzaIdx
)
353 if stanzaConfig
not in uniqueConfigs
:
354 uniqueConfigs
[ stanzaConfig
] = { "stanza" : stanza
, "idx" : stanzaIdx
}
356 print( stanzaId
+ stanzaConfig
+ stanza
.kvPairs_
[ "DESCRIPTION" ] )
358 # diff, value = Stanza.findFirstDifference( uniqueConfigs[ stanzaConfig ]["stanza"], stanza )
360 # print( stanzaId + stanzaConfig + "@{idx} diff => {value}".format( idx=uniqueConfigs[ stanzaConfig ][ "idx" ], value=value ) )
362 # print( stanzaId + stanzaConfig + "[no difference]" )
364 print( "!! - Compiler not found, some configurations will not work and will be hidden" )
365 stringSelection
= input( "Select configuration [0-{stop}] Default [0] (note !!) : ".format( stop
=( stanzaIdx
-1) ) )
366 idxSelection
= int( stringSelection
if stringSelection
.isdigit() else 0 )
367 if idxSelection
< 0 or idxSelection
> stanzaIdx
- 1 :
368 print( "Invalid configuration selection!" )
371 for stanza
in stanzas
:
372 if options
.preselect
.lower() in stanza
.kvPairs_
["DESCRIPTION"].lower() :
373 print( str( stanza
) + stanza
.kvPairs_
[ "DESCRIPTION"] )
377 if idxSelection
== len( stanzas
) :
378 print( "Error: Stanza configuration with description '{0}' does not exist. Preselect failed.".format( options
.preselect
) )
381 stanzaCfg
= stanzas
[idxSelection
]
385 ########################################################################################################################
387 ## Select enum-like string for string-based cmake options
389 ########################################################################################################################
390 def getStringOptionSelection( topLevelCmake
, searchString
, destinationOption
, defaultIndex
=0 ) :
391 topLevelCmakeFP
= open( topLevelCmake
, "r" )
392 topLevelCmakeLines
= topLevelCmakeFP
.read()
393 topLevelCmakeFP
.close()
395 stringOptionsMatch
= re
.search(
396 r
"set\s*[(]\s*" + searchString
+ r
"\s*(.*?)[)]",
400 if stringOptionsMatch
is None :
401 print( "Syntax error in parsing " + searchString
+ " from " + topLevelCmake
)
404 options
= [ option
.split( "#", 1 )[0].strip() for option
in stringOptionsMatch
.group(1).split( "\n" ) ]
406 options
= [ option
for option
in options
if option
]
408 optionsFmt
= "\n\t" + "\n\t".join( [ "{idx} : {opt}".format( idx
=options
.index( opt
), opt
=opt
) for opt
in options
] )
409 stringSelection
= input( "Select option for {option} from {optionsSource} [0-{max}] {opts} \nDefault [{defIdx}] : ".format(
410 option
=destinationOption
,
411 optionsSource
=searchString
,
417 selection
= int( stringSelection
if stringSelection
.isdigit() else defaultIndex
)
419 if selection
< 0 or selection
> len(options
) :
420 print( "Invalid option selection for " + searchString
+ "!" )
423 return options
[selection
]
425 ########################################################################################################################
427 ## Aggregate and allow toggle of various suboptions in alternate menu
429 ########################################################################################################################
430 def getSubOptions( topLevelCmake
, ignoreOptions
) :
431 topLevelCmakeFP
= open( topLevelCmake
, "r" )
432 topLevelCmakeLines
= topLevelCmakeFP
.read()
433 topLevelCmakeFP
.close()
435 stringOptionsMatch
= re
.finditer(
436 r
"set\s*[(]\s*(\w+)\s*(ON|OFF)\s*CACHE\s*BOOL\s*\"(.*?
)\"\s
*[)]",
440 # Remove commented ones and ones that don't follow pattern set( <OPT> ON|OFF CACHE BOOL "<OPT
>" )
441 options = [ [ option.group( 1 ), option.group( 2 ) ] for option in stringOptionsMatch if option.group( 1 ) == option.group( 3 ) and option.group(0).split( "#", 1 )[0].strip() ]
443 # Remove ignore options
444 options
= [ option
for option
in options
if option
[0] not in ignoreOptions
]
448 subOptionQuit
= False
452 optionStr
= "{idx:<3} {option:<24} : {value:<5}"
453 print( optionStr
.format( idx
="ID", option
="Option", value
="Default" ) )
455 print( optionStr
.format( idx
=options
.index(opt
), option
=opt
[0], value
=opt
[1] ) )
457 print( "Enter ID to toggle option on or off, q to quit : " )
458 # Loop until q, toggle from default not current value
459 while not subOptionQuit
:
460 optionToggleIdx
= input()
462 optionToggleIdx
= int( optionToggleIdx
)
463 if optionToggleIdx
< 0 or optionToggleIdx
>= len( options
) :
464 print( "Not a valid index" )
466 subOptions
[ options
[optionToggleIdx
][0] ] = "ON" if not ( options
[optionToggleIdx
][1] == "ON" ) else "OFF"
467 print( "Set {option} to {value}".format( option
=options
[optionToggleIdx
][0], value
=subOptions
[ options
[optionToggleIdx
][0] ] ) )
468 except ValueError as err
:
469 subOptionQuit
= optionToggleIdx
.lower() == "q"
475 parser
= getOptionsParser()
477 parser
.parse_args( namespace
=options
)
479 stanzaCfg
= selectStanza( options
)
481 additionalOptions
= {}
482 if not options
.skipCMakeOptions
:
483 if options
.sourceCMakeFile
is None :
484 print( "Error: Project source cmake file required for project specific options." )
487 additionalOptions
= projectSpecificOptions( options
, stanzaCfg
)
489 generateCMakeToolChainFile( options
.cmakeTemplateFile
, options
.outputConfigFile
, stanzaCfg
, additionalOptions
)
491 ########################################################################################################################
492 ########################################################################################################################
494 ## ABOVE THIS BREAK THINGS ARE EXACTLY THE SAME AS WRF/WPS
495 ## BELOW THIS BREAK THINGS DIFFER
497 ########################################################################################################################
498 ########################################################################################################################
500 def generateCMakeToolChainFile( cmakeToolChainTemplate
, output
, stanza
, optionsDict
={} ) :
501 cmakeToolChainTemplateFP
= open( cmakeToolChainTemplate
, "r" )
502 cmakeToolChainTemplateLines
= cmakeToolChainTemplateFP
.read()
503 cmakeToolChainTemplateFP
.close()
505 configStanza
= cmakeToolChainTemplateLines
.format(
506 ARCH_LOCAL
=stanza
.kvPairs_
["ARCH_LOCAL"],
507 LDFLAGS_LOCAL
=stanza
.kvPairs_
["LDFLAGS_LOCAL"],
508 BYTESWAPIO
=stanza
.kvPairs_
["BYTESWAPIO"],
509 CFLAGS_LOCAL
=stanza
.kvPairs_
["CFLAGS_LOCAL"],
510 DM_CC
=stanza
.kvPairs_
["DM_CC"],
511 DM_FC
=stanza
.kvPairs_
["DM_FC"],
512 DM_FC_FLAGS
=stanza
.kvPairs_
["DM_FC_FLAGS"],
513 DM_CC_FLAGS
=stanza
.kvPairs_
["DM_CC_FLAGS"],
514 FCBASEOPTS
=stanza
.kvPairs_
["FCBASEOPTS"],
515 FCDEBUG
=stanza
.kvPairs_
["FCDEBUG"],
516 FCNOOPT
=stanza
.kvPairs_
["FCNOOPT"],
517 FCOPTIM
=stanza
.kvPairs_
["FCOPTIM"],
518 M4_FLAGS
=stanza
.kvPairs_
["M4_FLAGS"],
519 SCC
=stanza
.kvPairs_
["SCC"],
520 SFC
=stanza
.kvPairs_
["SFC"],
521 SCC_FLAGS
=stanza
.kvPairs_
["SCC_FLAGS"],
522 SFC_FLAGS
=stanza
.kvPairs_
["SFC_FLAGS"],
523 CPP
=stanza
.kvPairs_
["CPP"],
524 CPP_FLAGS
=stanza
.kvPairs_
["CPP_FLAGS"],
527 # Extra stufff not from stanza but options
528 fmtOption
= "set( {opt:<32} {value:<12} CACHE STRING \"Set by configuration\" FORCE )"
529 configStanza
+= "\n" + "\n".join( [ fmtOption
.format( opt
=key
, value
=value
) for key
, value
in optionsDict
.items() ] )
531 outputFP
= open( output
, "w" )
532 outputFP
.write( configStanza
)
535 def projectSpecificOptions( options
, stanzaCfg
) :
536 coreOption
= getStringOptionSelection( options
.sourceCMakeFile
, "WRF_CORE_OPTIONS", "WRF_CORE" )
537 if coreOption
== "ARW" :
538 nestingOption
= getStringOptionSelection( options
.sourceCMakeFile
, "WRF_NESTING_OPTIONS", "WRF_NESTING", 1 )
539 caseOption
= getStringOptionSelection( options
.sourceCMakeFile
, "WRF_CASE_OPTIONS", "WRF_CASE" )
541 nestingOption
= "NONE"
545 yesValues
= [ "yes", "y", "true", "1" ]
546 # Acceptable no values
547 noValues
= [ "no", "n", "false", "0" ]
549 ##############################################################################
550 # Decompose the weird way to write the logic for DM/SM
552 if ( stanzaCfg
.serialOpt_
or stanzaCfg
.smparOpt_
) and ( stanzaCfg
.dmparOpt_
or stanzaCfg
.dmsmOpt_
) :
554 # we can safely check this since the user would not have been able to select this stanza if it couldn't be disabled
555 if stanzaCfg
.dmCompilersAvailable() :
556 useMPI
= not( input( "[DM] Use MPI? Default [Y] [Y/n] : " ).lower() in noValues
)
560 # User has no choice in the matter
561 useMPI
= ( stanzaCfg
.dmparOpt_
or stanzaCfg
.dmsmOpt_
)
564 if ( stanzaCfg
.serialOpt_
or stanzaCfg
.dmparOpt_
) and ( stanzaCfg
.smparOpt_
or stanzaCfg
.dmsmOpt_
):
566 useOpenMP
= input( "[SM] Use OpenMP? Default [N] [y/N] : " ).lower() in yesValues
568 # User has no choice in the matter
569 useOpenMP
= ( stanzaCfg
.smparOpt_
or stanzaCfg
.dmsmOpt_
)
571 ##############################################################################
573 alreadyAsked
= [ "USE_MPI", "USE_OPENMP" ]
574 doSuboptionMenu
= input( "Configure additional options? Default [N] [y/N] : " ).lower() in yesValues
577 subOptions
= getSubOptions( options
.sourceCMakeFile
, alreadyAsked
)
579 additionalOptions
= {
580 "WRF_CORE" : coreOption
,
581 "WRF_NESTING" : nestingOption
,
582 "WRF_CASE" : caseOption
,
583 "USE_MPI" : "ON" if useMPI
else "OFF",
584 "USE_OPENMP" : "ON" if useOpenMP
else "OFF",
586 additionalOptions
.update( subOptions
)
588 return additionalOptions
590 if __name__
== '__main__' :