print("running extra...") import gzip import shutil import os import sys import inspect import json import glob from datetime import datetime import re import pprint from platformio.project.config import ProjectConfig from platformio.project.exception import InvalidProjectConfError Import("env") #print(env.Dump()) OWN_FILE="extra_script.py" GEN_DIR='lib/generated' CFG_FILE='web/config.json' XDR_FILE='web/xdrconfig.json' INDEXJS="index.js" INDEXCSS="index.css" CFG_INCLUDE='GwConfigDefinitions.h' CFG_INCLUDE_IMPL='GwConfigDefImpl.h' XDR_INCLUDE='GwXdrTypeMappings.h' TASK_INCLUDE='GwUserTasks.h' GROVE_CONFIG="GwM5GroveGen.h" GROVE_CONFIG_IN="lib/hardware/GwM5Grove.in" EMBEDDED_INCLUDE="GwEmbeddedFiles.h" def getEmbeddedFiles(env): rt=[] efiles=env.GetProjectOption("board_build.embed_files") for f in efiles.split("\n"): if f == '': continue rt.append(f) return rt def basePath(): #see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined return os.path.dirname(inspect.getfile(lambda: None)) def outPath(): return os.path.join(basePath(),GEN_DIR) def checkDir(): dn=outPath() if not os.path.exists(dn): os.makedirs(dn) if not os.path.isdir(dn): print("unable to create %s"%dn) return False return True def isCurrent(infile,outfile): if os.path.exists(outfile): otime=os.path.getmtime(outfile) itime=os.path.getmtime(infile) if (otime >= itime): own=os.path.join(basePath(),OWN_FILE) if os.path.exists(own): owntime=os.path.getmtime(own) if owntime > otime: return False print("%s is newer then %s, no need to recreate"%(outfile,infile)) return True return False def compressFile(inFile,outfile): if isCurrent(inFile,outfile): return print("compressing %s"%inFile) with open(inFile, 'rb') as f_in: with gzip.open(outfile, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) def generateFile(infile,outfile,callback,inMode='rb',outMode='w'): if isCurrent(infile,outfile): return print("creating %s"%outfile) oh=None with open(infile,inMode) as ch: with open(outfile,outMode) as oh: try: callback(ch,oh,inFile=infile) oh.close() except Exception as e: try: oh.close() except: pass os.unlink(outfile) raise def writeFileIfChanged(fileName,data): if os.path.exists(fileName): with open(fileName,"r") as ih: old=ih.read() ih.close() if old == data: return False print("#generating %s"%fileName) with open(fileName,"w") as oh: oh.write(data) return True def mergeConfig(base,other): try: customconfig = env.GetProjectOption("custom_config") except InvalidProjectConfError: customconfig = None for bdir in other: if customconfig and os.path.exists(os.path.join(bdir,customconfig)): cname=os.path.join(bdir,customconfig) print("merge custom config {}".format(cname)) with open(cname,'rb') as ah: base += json.load(ah) continue cname=os.path.join(bdir,"config.json") if os.path.exists(cname): print("merge config %s"%cname) with open(cname,'rb') as ah: merge=json.load(ah) base=base+merge return base def replaceTexts(data,replacements): if replacements is None: return data if isinstance(data,str): for k,v in replacements.items(): data=data.replace("$"+k,str(v)) return data if isinstance(data,list): rt=[] for e in data: rt.append(replaceTexts(e,replacements)) return rt if isinstance(data,dict): rt={} for k,v in data.items(): rt[replaceTexts(k,replacements)]=replaceTexts(v,replacements) return rt return data def expandConfig(config): rt=[] for item in config: type=item.get('type') if type != 'array': rt.append(item) continue replacements=item.get('replace') children=item.get('children') name=item.get('name') if name is None: name="#unknown#" if not isinstance(replacements,list): raise Exception("missing replacements at array %s"%name) for replace in replacements: if children is not None: for c in children: rt.append(replaceTexts(c,replace)) return rt def generateMergedConfig(inFile,outFile,addDirs=[]): if not os.path.exists(inFile): raise Exception("unable to read cfg file %s"%inFile) data="" with open(inFile,'rb') as ch: config=json.load(ch) config=mergeConfig(config,addDirs) config=expandConfig(config) data=json.dumps(config,indent=2) writeFileIfChanged(outFile,data) def generateCfg(inFile,outFile,impl): if not os.path.exists(inFile): raise Exception("unable to read cfg file %s"%inFile) data="" with open(inFile,'rb') as ch: config=json.load(ch) data+="//generated from %s\n"%inFile l=len(config) idx=0 if not impl: data+='#include "GwConfigItem.h"\n' data+='class GwConfigDefinitions{\n' data+=' public:\n' data+=' int getNumConfig() const{return %d;}\n'%(l) for item in config: n=item.get('name') if n is None: continue if len(n) > 15: raise Exception("%s: config names must be max 15 caracters"%n) data+=' static constexpr const char* %s="%s";\n'%(n,n) data+="};\n" else: data+='void GwConfigHandler::populateConfigs(GwConfigInterface **config){\n' for item in config: name=item.get('name') if name is None: continue data+=' configs[%d]='%(idx) idx+=1 secret="false"; if item.get('type') == 'password': secret="true" data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret) data+='}\n' writeFileIfChanged(outFile,data) def labelFilter(label): return re.sub("[^a-zA-Z0-9]","",re.sub("\([0-9]*\)","",label)) def generateXdrMappings(fp,oh,inFile=''): jdoc=json.load(fp) oh.write("static GwXDRTypeMapping* typeMappings[]={\n") first=True for cat in jdoc: item=jdoc[cat] cid=item.get('id') if cid is None: continue tc=item.get('type') if tc is not None: if first: first=False else: oh.write(",\n") oh.write(" new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat)) fields=item.get('fields') if fields is None: continue idx=0 for fe in fields: if not isinstance(fe,dict): continue tc=fe.get('t') id=fe.get('v') if id is None: id=idx idx+=1 l=fe.get('l') or '' if tc is None or id is None: continue if first: first=False else: oh.write(",\n") oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,id,tc,cat,l)) oh.write("\n") oh.write("};\n") for cat in jdoc: item=jdoc[cat] cid=item.get('id') if cid is None: continue selectors=item.get('selector') if selectors is not None: for selector in selectors: label=selector.get('l') value=selector.get('v') if label is not None and value is not None: label=labelFilter(label) define=("GWXDRSEL_%s_%s"%(cat,label)).upper() oh.write(" #define %s %s\n"%(define,value)) fields=item.get('fields') if fields is not None: idx=0 for field in fields: v=field.get('v') if v is None: v=idx else: v=int(v) label=field.get('l') if v is not None and label is not None: define=("GWXDRFIELD_%s_%s"%(cat,labelFilter(label))).upper(); oh.write(" #define %s %s\n"%(define,str(v))) idx+=1 class Grove: def __init__(self,name) -> None: self.name=name def _ss(self,z=False): if z: return self.name return self.name if self.name != 'Z' else '' def _suffix(self): return '_'+self.name if self.name != 'Z' else '' def replace(self,line): if line is None: return line return line.replace('$G$',self._ss()).replace('$Z$',self._ss(True)).replace('$GS$',self._suffix()) def generateGroveDefs(inh,outh,inFile=''): GROVES=[Grove('Z'),Grove('A'),Grove('B'),Grove('C')] definition=[] started=False def writeConfig(): for grove in GROVES: for cl in definition: outh.write(grove.replace(cl)) for line in inh: if re.match(" *#GROVE",line): started=True if len(definition) > 0: writeConfig() definition=[] continue if started: definition.append(line) if len(definition) > 0: writeConfig() userTaskDirs=[] def getUserTaskDirs(): rt=[] taskdirs=glob.glob(os.path.join( basePath(),'lib','*task*')) for task in taskdirs: rt.append(task) return rt def checkAndAdd(file,names,ilist): if not file.endswith('.h'): return match=False for cmp in names: #print("##check %s<->%s"%(f.lower(),cmp)) if file.lower() == cmp: match=True if not match: return ilist.append(file) def genereateUserTasks(outfile): includes=[] for task in userTaskDirs: #print("##taskdir=%s"%task) base=os.path.basename(task) includeNames=[base.lower()+".h",'gw'+base.lower()+'.h'] for f in os.listdir(task): checkAndAdd(f,includeNames,includes) includeData="" for i in includes: print("#task include %s"%i) includeData+="#include <%s>\n"%i writeFileIfChanged(outfile,includeData) def generateEmbedded(elist,outFile): content="" for entry in elist: content+="EMBED_GZ_FILE(\"%s\",%s,\"%s\");\n"%entry writeFileIfChanged(outFile,content) def getContentType(fn): if (fn.endswith('.gz')): fn=fn[0:-3] if (fn.endswith('html')): return "text/html" if (fn.endswith('json')): return "application/json" if (fn.endswith('js')): return "text/javascript" if (fn.endswith('css')): return "text/css" return "application/octet-stream" def getLibs(): base=os.path.join(basePath(),"lib") rt=[] for sd in os.listdir(base): if sd == '..': continue if sd == '.': continue fn=os.path.join(base,sd) if os.path.isdir(fn): rt.append(sd) EXTRAS=['generated'] for e in EXTRAS: if not e in rt: rt.append(e) return rt def joinFiles(target,pattern,dirlist): flist=[] for dir in dirlist: fn=os.path.join(dir,pattern) if os.path.exists(fn): flist.append(fn) current=False if os.path.exists(target): current=True for f in flist: if not isCurrent(f,target): current=False break if current: print("%s is up to date"%target) return print("creating %s"%target) with gzip.open(target,"wb") as oh: for fn in flist: print("adding %s to %s"%(fn,target)) with open(fn,"rb") as rh: shutil.copyfileobj(rh,oh) OWNLIBS=getLibs()+["FS","WiFi"] GLOBAL_INCLUDES=[] def handleDeps(env): #overwrite the GetProjectConfig #to inject all our libs oldGetProjectConfig=env.GetProjectConfig def GetProjectConfigX(env): rt=oldGetProjectConfig() cenv="env:"+env['PIOENV'] libs=[] for section,options in rt.as_tuple(): if section == cenv: for key,values in options: if key == 'lib_deps': libs=values mustUpdate=False for lib in OWNLIBS: if not lib in libs: libs.append(lib) mustUpdate=True if mustUpdate: update=[(cenv,[('lib_deps',libs)])] rt.update(update) return rt env.AddMethod(GetProjectConfigX,"GetProjectConfig") #store the list of all includes after we resolved #the dependencies for our main project #we will use them for all compilations afterwards oldLibBuilder=env.ConfigureProjectLibBuilder def ConfigureProjectLibBuilderX(env): global GLOBAL_INCLUDES project=oldLibBuilder() #print("##ConfigureProjectLibBuilderX") #pprint.pprint(project) if project.depbuilders: #print("##depbuilders %s"%",".join(map(lambda x: x.path,project.depbuilders))) for db in project.depbuilders: idirs=db.get_include_dirs() for id in idirs: if not id in GLOBAL_INCLUDES: GLOBAL_INCLUDES.append(id) return project env.AddMethod(ConfigureProjectLibBuilderX,"ConfigureProjectLibBuilder") def injectIncludes(env,node): return env.Object( node, CPPPATH=env["CPPPATH"]+GLOBAL_INCLUDES ) env.AddBuildMiddleware(injectIncludes) def prebuild(env): global userTaskDirs print("#prebuild running") if not checkDir(): sys.exit(1) ldf_mode=env.GetProjectOption("lib_ldf_mode") if ldf_mode == 'off': print("##ldf off - own dependency handling") handleDeps(env) userTaskDirs=getUserTaskDirs() mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE)) generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs) compressFile(mergedConfig,mergedConfig+".gz") generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False) generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True) joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs) joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs) embedded=getEmbeddedFiles(env) filedefs=[] for ef in embedded: print("#checking embedded file %s"%ef) (dn,fn)=os.path.split(ef) pureName=fn if pureName.endswith('.gz'): pureName=pureName[0:-3] ct=getContentType(pureName) usname=ef.replace('/','_').replace('.','_') filedefs.append((pureName,usname,ct)) inFile=os.path.join(basePath(),"web",pureName) if os.path.exists(inFile): compressFile(inFile,ef) else: print("#WARNING: infile %s for %s not found"%(inFile,ef)) generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE)) genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE)) generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings) generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r') version="dev"+datetime.now().strftime("%Y%m%d") env.Append(CPPDEFINES=[('GWDEVVERSION',version)]) def cleangenerated(source, target, env): od=outPath() if os.path.isdir(od): print("#cleaning up %s"%od) for f in os.listdir(od): if f == "." or f == "..": continue fn=os.path.join(od,f) os.unlink(f) print("#prescript...") prebuild(env) board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper() print("Board=#%s#"%board) print("BuildFlags=%s"%(" ".join(env["BUILD_FLAGS"]))) env.Append( LINKFLAGS=[ "-u", "custom_app_desc" ], CPPDEFINES=[(board,"1")] ) #script does not run on clean yet - maybe in the future env.AddPostAction("clean",cleangenerated)