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)" )
24 def __init__( self
, lines
) :
28 self
.osArchLine_
= None
31 self
.crossPlatform_
= False
32 self
.skipCrossPlatform_
= True
33 self
.serialOpt_
= False
34 self
.smparOpt_
= False
35 self
.dmparOpt_
= False
39 self
.osArchLine_
= self
.lines_
.partition("\n")[0]
40 # First get os & archs
41 osarchMatch
= osAndArch
.match( self
.osArchLine_
)
43 if osarchMatch
is None :
44 osarchMatch
= osAndArchAlt
.match( self
.osArchLine_
)
45 if osarchMatch
is None :
46 print( "Could not find OS and architecture info in " + self
.osArchLine_
)
48 self
.os_
= osarchMatch
.group(1)
49 self
.archs_
= osarchMatch
.group(2).strip(",").split( " " )
51 if ( self
.os_
.lower() != platform
.system().lower() or
52 platform
.machine() not in self
.archs_
) :
53 self
.crossPlatform_
= True
55 # Allow cross platform or must not be cross platform
56 if not self
.skipCrossPlatform_
or ( self
.skipCrossPlatform_
and not self
.crossPlatform_
) :
58 # Find OpenMP/MPI compilation options
59 memOpts
= self
.osArchLine_
.partition( "#" )[-1].split( " " )
61 self
.serialOpt_
= "serial" in memOpts
62 self
.smparOpt_
= "smpar" in memOpts
63 self
.dmparOpt_
= "dmpar" in memOpts
64 self
.dmsmOpt_
= "dm+sm" in memOpts
66 for kvPairMatch
in kvPair
.finditer( self
.lines_
) :
67 self
.kvPairs_
[ kvPairMatch
.group(1) ] = kvPairMatch
.group(2)
68 self
.removeComments( kvPairMatch
.group(1) )
73 ######################################################################################################################
75 ## search and replace $(<var>) and $<var> instances
77 ######################################################################################################################
78 def dereference( self
, field
, fatal
=False ) :
79 # print( "Dereferencing " + field )
81 if field
in self
.kvPairs_
:
82 prevField
= self
.kvPairs_
[field
]
84 for refVarIter
in referenceVar
.finditer( prevField
) :
87 if refVarIter
is not None :
88 # Grab group 1 and check that it is in our kv pairs
89 refVar
= refVarIter
.group(2)
90 # print( "Found variable {0} in field {1}".format( refVar, field ) )
91 if refVar
not in self
.kvPairs_
:
92 # Try to use the environment variables
93 if refVar
in os
.environ
:
94 envSub
= os
.environ
[ refVar
]
97 # print( "Could not rereference : " + refVar )
103 # This is an environment variable
104 if envSub
is not None :
105 self
.kvPairs_
[field
] = self
.kvPairs_
[field
].replace(
106 "{var}".format( var
=refVarIter
.group(0) ),
109 # This is a kv pair, recurse
112 self
.dereference( refVar
, fatal
)
114 # Replace in original
115 self
.kvPairs_
[field
] = self
.kvPairs_
[field
].replace(
116 "{var}".format( var
=refVarIter
.group(0) ),
117 self
.kvPairs_
[refVar
]
120 def removeReferences( self
, field
, specifics
=[] ) :
121 if field
in self
.kvPairs_
:
123 for specific
in specifics
:
124 self
.kvPairs_
[ field
] = self
.kvPairs_
[ field
].replace(
125 "$({var})".format( var
=specific
),
129 self
.kvPairs_
[ field
] = referenceVar
.sub( "", self
.kvPairs_
[ field
] )
132 def removeComments( self
, field
) :
133 if field
in self
.kvPairs_
:
134 self
.kvPairs_
[ field
] = self
.kvPairs_
[ field
].split( "#", 1 )[0]
136 def splitIntoFieldAndFlags( self
, field
) :
137 # Fix flags being mixed with programs
138 if field
in self
.kvPairs_
:
139 fieldValue
= self
.kvPairs_
[ field
]
141 self
.kvPairs_
[field
] = fieldValue
.partition(" ")[0]
142 self
.kvPairs_
[field
+ "_FLAGS"] = fieldValue
.partition(" ")[1]
144 ######################################################################################################################
146 ## Clean up the stanza so kv pairs can be used as-is
148 ######################################################################################################################
149 def sanitize( self
) :
150 # Fix problematic variables
151 self
.dereference( "DM_FC" )
152 self
.dereference( "DM_CC" )
153 self
.removeReferences( "FCBASEOPTS_NO_G" )
154 # Get rid of all these mixed up flags, these are handled by cmake natively or
155 # just in the wrong place
156 self
.removeReferences( "FCBASEOPTS", [ "FCDEBUG", "FORMAT_FREE", "BYTESWAPIO", ] )
157 self
.removeReferences( "FFLAGS", [ "FORMAT_FREE", "FORMAT_FIXED" ] )
158 self
.removeReferences( "F77FLAGS", [ "FORMAT_FREE", "FORMAT_FIXED" ] )
160 self
.dereference( "FCBASEOPTS" )
162 # Remove rogue compile commands that should *NOT* even be here
196 for keyToSan
in keysToSanitize
:
197 if keyToSan
in self
.kvPairs_
:
198 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_COMP_L", "" )
199 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_COMP_I", "" )
200 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_FC", "" )
201 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_CC", "" )
202 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_FDEFS", "" )
203 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_MPI", "" )
204 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_COMPAT_FLAGS", "" )
205 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_CPPFLAGS", "" )
206 self
.kvPairs_
[ keyToSan
] = self
.kvPairs_
[ keyToSan
].replace( "CONFIGURE_TRADFLAG", "" )
208 self
.kvPairs_
[ keyToSan
] = compileObject
.sub( r
"\1\2", self
.kvPairs_
[ keyToSan
] ).strip()
211 # Now fix certain ones that are mixing programs with flags all mashed into one option
212 self
.splitIntoFieldAndFlags( "SFC" )
213 self
.splitIntoFieldAndFlags( "SCC" )
214 self
.splitIntoFieldAndFlags( "DM_FC" )
215 self
.splitIntoFieldAndFlags( "DM_CC" )
216 self
.splitIntoFieldAndFlags( "CPP" )
217 self
.splitIntoFieldAndFlags( "M4" )
219 # Now deref all the rest
220 for key
in self
.kvPairs_
:
221 self
.dereference( key
)
222 # And for final measure strip
223 self
.kvPairs_
[ key
] = self
.kvPairs_
[ key
].strip()
225 def serialCompilersAvailable( self
) :
226 return which( self
.kvPairs_
["SFC"] ) is not None and which( self
.kvPairs_
["SCC"] ) is not None
228 def dmCompilersAvailable( self
) :
229 return which( self
.kvPairs_
["DM_FC"] ) is not None and which( self
.kvPairs_
["DM_CC"] ) is not None
231 ######################################################################################################################
233 ## string representation to view as option
235 ######################################################################################################################
237 # base = """OS {os:<8} ARCHITECTURES {archs:<20}
240 # >> CCOMP = {CCOMP:<12}
241 # >> DM_FC = {DM_FC:<12}
242 # >> DM_CC = {DM_CC:<12}
244 base
= """ {os:<10} {recSFC} {SFC:<11} / {recSCC} {SCC:<11} / {recDM_FC} {DM_FC:<11} / {recDM_CC} {DM_CC:<11}"""
245 text
= inspect
.cleandoc( base
).format(
247 recSFC
=( "!!" if which( self
.kvPairs_
["SFC"] ) is None else (" " * 2 ) ),
248 recSCC
=( "!!" if which( self
.kvPairs_
["SCC"] ) is None else (" " * 2 ) ),
249 recDM_FC
=( "!!" if which( self
.kvPairs_
["DM_FC"] ) is None else (" " * 2 ) ),
250 recDM_CC
=( "!!" if which( self
.kvPairs_
["DM_CC"] ) is None else (" " * 2 ) ),
251 # archs=str(self.archs_),
252 SFC
=str( self
.kvPairs_
["SFC"] ),
253 SCC
=str( self
.kvPairs_
["SCC"] ),
254 DM_FC
=str( self
.kvPairs_
["DM_FC"] ),
255 DM_CC
=str( self
.kvPairs_
["DM_CC"] )
257 # text += "\n" + "\n".join( [ "{key:<18} = {value}".format( key=key, value=value) for key, value in self.kvPairs_.items() ] )
260 ######################################################################################################################
262 ## Find first apparent difference between two stanzas
264 ######################################################################################################################
266 def findFirstDifference( rhStanza
, lhStanza
, maxLength
=32 ) :
287 for rhKey
, rhValue
in rhStanza
.kvPairs_
.items() :
288 if rhKey
in valuesToCheck
and rhKey
in lhStanza
.kvPairs_
:
289 # Qualifies for difference
290 if rhValue
!= lhStanza
.kvPairs_
[rhKey
] :
292 value
= "{key:<12} = {value}".format( key
=rhKey
, value
=lhStanza
.kvPairs_
[rhKey
] )
295 value
= ( value
[:maxLength
] + "..." ) if len( value
) > maxLength
else value
299 ########################################################################################################################
303 ########################################################################################################################
304 def getOptionsParser() :
305 parser
= argparse
.ArgumentParser( )
307 # https://stackoverflow.com/a/24181138
308 requiredNamed
= parser
.add_argument_group( "required named arguments" )
310 requiredNamed
.add_argument(
313 help="configure.defaults file holding all stanza configurations",
317 requiredNamed
.add_argument(
319 dest
="cmakeTemplateFile",
320 help="cmake template file for configuring stanza into cmake syntax",
324 requiredNamed
.add_argument(
326 dest
="outputConfigFile",
327 help="cmake output toolchain config file for selected stanza",
335 help="Use preselected stanza configuration, if multiple match grabs the first one",
341 "-x", "--skipCMakeOptions",
342 dest
="skipCMakeOptions",
343 help="Skip query of available CMake options",
350 dest
="sourceCMakeFile",
351 help="Required unless -x/--skipCMakeOptions set, project cmake source file used to determine available options",
359 class Options(object):
360 """Empty namespace"""
363 ########################################################################################################################
365 ## Select stanza to operate on
367 ########################################################################################################################
368 def selectStanza( options
) :
370 fp
= open( options
.configFile
, 'r' )
374 # Now grab the blocks and parse
376 # Gather all stanzas available
377 for stanzaBlock
in archBlock
.finditer( lines
) :
378 stanza
= Stanza( stanzaBlock
.group(1) )
381 if not stanza
.crossPlatform_
and stanza
.serialCompilersAvailable() and ( stanza
.dmCompilersAvailable() or ( stanza
.serialOpt_
or stanza
.smparOpt_
) ) :
382 if "DESCRIPTION" not in stanza
.kvPairs_
:
383 # Of course WPS configure.defaults is different than WRF so descriptions are embedded in the comments
384 stanza
.kvPairs_
[ "DESCRIPTION" ] = stanza
.osArchLine_
.partition( "," )[ -1 ].partition( "#" )[0].strip()
385 stanzas
.append( stanza
)
388 if options
.preselect
is None :
392 for stanza
in stanzas
:
393 stanzaConfig
= str( stanza
)
394 stanzaId
= "{idx:<3} ".format( idx
=stanzaIdx
)
395 if stanzaConfig
not in uniqueConfigs
:
396 uniqueConfigs
[ stanzaConfig
] = { "stanza" : stanza
, "idx" : stanzaIdx
}
398 print( stanzaId
+ stanzaConfig
+ stanza
.kvPairs_
[ "DESCRIPTION" ] )
400 # diff, value = Stanza.findFirstDifference( uniqueConfigs[ stanzaConfig ]["stanza"], stanza )
402 # print( stanzaId + stanzaConfig + "@{idx} diff => {value}".format( idx=uniqueConfigs[ stanzaConfig ][ "idx" ], value=value ) )
404 # print( stanzaId + stanzaConfig + "[no difference]" )
406 print( "!! - Compiler not found, some configurations will not work and will be hidden" )
407 stringSelection
= input( "Select configuration [0-{stop}] Default [0] (note !!) : ".format( stop
=( stanzaIdx
-1) ) )
408 idxSelection
= int( stringSelection
if stringSelection
.isdigit() else 0 )
409 if idxSelection
< 0 or idxSelection
> stanzaIdx
- 1 :
410 print( "Invalid configuration selection!" )
413 for stanza
in stanzas
:
414 if options
.preselect
.lower() in stanza
.kvPairs_
["DESCRIPTION"].lower() :
415 print( str( stanza
) + stanza
.kvPairs_
[ "DESCRIPTION"] )
419 if idxSelection
== len( stanzas
) :
420 print( "Error: Stanza configuration with description '{0}' does not exist. Preselect failed.".format( options
.preselect
) )
423 stanzaCfg
= stanzas
[idxSelection
]
427 ########################################################################################################################
429 ## Select enum-like string for string-based cmake options
431 ########################################################################################################################
432 def getStringOptionSelection( topLevelCmake
, searchString
, destinationOption
, defaultIndex
=0 ) :
433 topLevelCmakeFP
= open( topLevelCmake
, "r" )
434 topLevelCmakeLines
= topLevelCmakeFP
.read()
435 topLevelCmakeFP
.close()
437 stringOptionsMatch
= re
.search(
438 r
"set\s*[(]\s*" + searchString
+ r
"\s*(.*?)[)]",
442 if stringOptionsMatch
is None :
443 print( "Syntax error in parsing " + searchString
+ " from " + topLevelCmake
)
446 options
= [ option
.split( "#", 1 )[0].strip() for option
in stringOptionsMatch
.group(1).split( "\n" ) ]
448 options
= [ option
for option
in options
if option
]
450 optionsFmt
= "\n\t" + "\n\t".join( [ "{idx} : {opt}".format( idx
=options
.index( opt
), opt
=opt
) for opt
in options
] )
451 stringSelection
= input( "Select option for {option} from {optionsSource} [0-{max}] {opts} \nDefault [{defIdx}] : ".format(
452 option
=destinationOption
,
453 optionsSource
=searchString
,
459 selection
= int( stringSelection
if stringSelection
.isdigit() else defaultIndex
)
461 if selection
< 0 or selection
> len(options
) :
462 print( "Invalid option selection for " + searchString
+ "!" )
465 return options
[selection
]
467 ########################################################################################################################
469 ## Aggregate and allow toggle of various suboptions in alternate menu
471 ########################################################################################################################
472 def getSubOptions( topLevelCmake
, ignoreOptions
) :
473 topLevelCmakeFP
= open( topLevelCmake
, "r" )
474 topLevelCmakeLines
= topLevelCmakeFP
.read()
475 topLevelCmakeFP
.close()
477 stringOptionsMatch
= re
.finditer(
478 r
"set\s*[(]\s*(\w+)\s*(ON|OFF)\s*CACHE\s*BOOL\s*\"(.*?
)\"\s
*[)]",
482 # Remove commented ones and ones that don't follow pattern set( <OPT> ON|OFF CACHE BOOL "<OPT
>" )
483 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() ]
485 # Remove ignore options
486 options
= [ option
for option
in options
if option
[0] not in ignoreOptions
]
490 subOptionQuit
= False
494 optionStr
= "{idx:<3} {option:<24} : {value:<5}"
495 print( optionStr
.format( idx
="ID", option
="Option", value
="Default" ) )
497 print( optionStr
.format( idx
=options
.index(opt
), option
=opt
[0], value
=opt
[1] ) )
499 print( "Enter ID to toggle option on or off, q to quit : " )
500 # Loop until q, toggle from default not current value
501 while not subOptionQuit
:
502 optionToggleIdx
= input()
504 optionToggleIdx
= int( optionToggleIdx
)
505 if optionToggleIdx
< 0 or optionToggleIdx
>= len( options
) :
506 print( "Not a valid index" )
508 subOptions
[ options
[optionToggleIdx
][0] ] = "ON" if not ( options
[optionToggleIdx
][1] == "ON" ) else "OFF"
509 print( "Set {option} to {value}".format( option
=options
[optionToggleIdx
][0], value
=subOptions
[ options
[optionToggleIdx
][0] ] ) )
510 except ValueError as err
:
511 subOptionQuit
= optionToggleIdx
.lower() == "q"
517 parser
= getOptionsParser()
519 parser
.parse_args( namespace
=options
)
521 stanzaCfg
= selectStanza( options
)
523 additionalOptions
= {}
524 if not options
.skipCMakeOptions
:
525 if options
.sourceCMakeFile
is None :
526 print( "Error: Project source cmake file required for project specific options." )
529 additionalOptions
= projectSpecificOptions( options
, stanzaCfg
)
531 generateCMakeToolChainFile( options
.cmakeTemplateFile
, options
.outputConfigFile
, stanzaCfg
, additionalOptions
)
533 ########################################################################################################################
534 ########################################################################################################################
536 ## ABOVE THIS BREAK THINGS ARE EXACTLY THE SAME AS WRF/WPS
537 ## BELOW THIS BREAK THINGS DIFFER
539 ########################################################################################################################
540 ########################################################################################################################
542 def generateCMakeToolChainFile( cmakeToolChainTemplate
, output
, stanza
, optionsDict
={} ) :
543 cmakeToolChainTemplateFP
= open( cmakeToolChainTemplate
, "r" )
544 cmakeToolChainTemplateLines
= cmakeToolChainTemplateFP
.read()
545 cmakeToolChainTemplateFP
.close()
547 configStanza
= cmakeToolChainTemplateLines
.format(
548 CPP
=stanza
.kvPairs_
["CPP"],
549 CPPFLAGS
=stanza
.kvPairs_
["CPPFLAGS"],
550 CPP_FLAGS
=stanza
.kvPairs_
["CPP_FLAGS"],
551 # BYTESWAPIO=stanza.kvPairs_["BYTESWAPIO"],
552 CFLAGS
=stanza
.kvPairs_
["CFLAGS"],
553 FFLAGS
=stanza
.kvPairs_
["FFLAGS"],
554 DM_CC
=stanza
.kvPairs_
["DM_CC"],
555 DM_FC
=stanza
.kvPairs_
["DM_FC"],
556 DM_FC_FLAGS
=stanza
.kvPairs_
["DM_FC_FLAGS"],
557 DM_CC_FLAGS
=stanza
.kvPairs_
["DM_CC_FLAGS"],
558 # FCBASEOPTS=stanza.kvPairs_["FCBASEOPTS"],
559 # FCDEBUG=stanza.kvPairs_["FCDEBUG"],
560 # FCNOOPT=stanza.kvPairs_["FCNOOPT"],
561 # FCOPTIM=stanza.kvPairs_["FCOPTIM"],
562 # M4_FLAGS=stanza.kvPairs_["M4_FLAGS"],
563 SCC
=stanza
.kvPairs_
["SCC"],
564 SFC
=stanza
.kvPairs_
["SFC"],
565 SCC_FLAGS
=stanza
.kvPairs_
["SCC_FLAGS"],
566 SFC_FLAGS
=stanza
.kvPairs_
["SFC_FLAGS"]
569 # Extra stufff not from stanza but options
570 fmtOption
= "set( {opt:<32} {value:<12} CACHE STRING \"Set by configuration\" FORCE )"
571 configStanza
+= "\n" + "\n".join( [ fmtOption
.format( opt
=key
, value
=value
) for key
, value
in optionsDict
.items() ] )
573 outputFP
= open( output
, "w" )
574 outputFP
.write( configStanza
)
577 def projectSpecificOptions( options
, stanzaCfg
) :
580 yesValues
= [ "yes", "y", "true", "1" ]
581 # Acceptable no values
582 noValues
= [ "no", "n", "false", "0" ]
584 ##############################################################################
585 # Decompose the weird way to write the logic for DM/SM
587 if ( stanzaCfg
.serialOpt_
or stanzaCfg
.smparOpt_
) and ( stanzaCfg
.dmparOpt_
or stanzaCfg
.dmsmOpt_
) :
589 # we can safely check this since the user would not have been able to select this stanza if it couldn't be disabled
590 if stanzaCfg
.dmCompilersAvailable() :
591 useMPI
= not( input( "[DM] Use MPI? Default [N] [y/N] : " ).lower() in yesValues
)
595 # User has no choice in the matter
596 useMPI
= ( stanzaCfg
.dmparOpt_
or stanzaCfg
.dmsmOpt_
)
599 if ( stanzaCfg
.serialOpt_
or stanzaCfg
.dmparOpt_
) and ( stanzaCfg
.smparOpt_
or stanzaCfg
.dmsmOpt_
):
601 useOpenMP
= input( "[SM] Use OpenMP? Default [N] [y/N] : " ).lower() in yesValues
603 # User has no choice in the matter
604 useOpenMP
= ( stanzaCfg
.smparOpt_
or stanzaCfg
.dmsmOpt_
)
606 ##############################################################################
608 buildExt
= input( "[GRIB2] Build GRIB2 libraries (zlib, libpng, JasPer) from source in external/? Default [N] [y/N] : " ).lower() in yesValues
609 useWRF
= not( input( "[WRF] Attempt to find compiled WRF model (needed for geogrid and metgrid)? Default [Y] [Y/n] : " ).lower() in noValues
)
611 additionalOptions
= {
612 # "WRF_CORE" : coreOption,
613 # "WRF_NESTING" : nestingOption,
614 # "WRF_CASE" : caseOption,
615 "USE_OPENMP" : "ON" if useOpenMP
else "OFF",
616 "USE_MPI" : "ON" if useMPI
else "OFF",
617 "BUILD_EXTERNALS" : "ON" if buildExt
else "OFF",
618 "USE_WRF" : "ON" if useWRF
else "OFF"
621 return additionalOptions
625 if __name__
== '__main__' :