reverted sky130/makefile.in, renamed files starting with 'og,' replaced hard-coded path in create_project with PREFIX
diff --git a/common/project_manager.py b/common/project_manager.py new file mode 100755 index 0000000..73ff0d8 --- /dev/null +++ b/common/project_manager.py
@@ -0,0 +1,4138 @@ +#!/usr/bin/env python3 -B +# +#-------------------------------------------------------- +# Open Galaxy Project Manager GUI. +# +# This is a Python tkinter script that handles local +# project management. It is meant as a replacement for +# appsel_zenity.sh +# +#-------------------------------------------------------- +# Written by Tim Edwards +# efabless, inc. +# September 9, 2016 +# Modifications 2017, 2018 +# Version 1.0 +#-------------------------------------------------------- + +import sys +# Require python 3.5.x (and not python 3.6.x). Without this trap here, in several +# instances of VMs where /usr/bin/python3 symlinked to 3.6.x by mistake, it manifests +# as (misleading) errors like: ImportError: No module named 'yaml' +# +# '%x' % sys.hexversion -> '30502f0' + +import tkinter +from tkinter import ttk, StringVar, Listbox, END +from tkinter import filedialog + +# globals +theProg = sys.argv[0] +root = tkinter.Tk() # WARNING: must be exactly one instance of Tk; don't call again elsewhere + +# 4 configurations based on booleans: splash,defer +# n,n: no splash, show only form when completed: LEGACY MODE, user confused by visual lag. +# n,y: no splash but defer projLoad: show an empty form ASAP +# y,n: yes splash, and wait for projLoad before showing completed form +# y,y: yes splash, but also defer projLoad: show empty form ASAP + +# deferLoad = False # LEGACY: no splash, and wait for completed form +# doSplash = False + +deferLoad = True # True: display GUI before (slow) loading of projects, so no splash: +doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects + +# deferLoad = False # load projects before showing form, so need splash: +# doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects + +# deferLoad = True # here keep splash also, despite also deferred-loading +# doSplash = True + +#------------------------------------------------------ +# Splash screen: display ASAP: BEFORE bulk of imports. +#------------------------------------------------------ + +class SplashScreen(tkinter.Toplevel): + """Open Galaxy Project Management Splash Screen""" + + def __init__(self, parent, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + parent.withdraw() + #EFABLESS PLATFORM + #image = tkinter.PhotoImage(file="/ef/efabless/opengalaxy/og_splashscreen50.gif") + label = ttk.Label(self, image=image) + label.pack() + + # required to make window show before the program gets to the mainloop + self.update_idletasks() + +import faulthandler +import signal + +# SplashScreen here. fyi: there's a 2nd/later __main__ section for main app +splash = None # a global +if __name__ == '__main__': + faulthandler.register(signal.SIGUSR2) + if doSplash: + splash = SplashScreen(root) + +import io +import os +import re +import json +import yaml +import shutil +import tarfile +import datetime +import subprocess +import contextlib +import tempfile +import glob + +import tksimpledialog +import tooltip +from rename_project import rename_project_all +#from fix_libdirs import fix_libdirs +from consoletext import ConsoleText +from helpwindow import HelpWindow +from treeviewchoice import TreeViewChoice +from symbolbuilder import SymbolBuilder +from make_icon_from_soft import create_symbol +from profile import Profile + +import config + +# Global name for design directory +designdir = 'design' +# Global name for import directory +importdir = 'import' +# Global name for cloudv directory +cloudvdir = 'cloudv' +# Global name for archived imports project sub-directory +archiveimportdir = 'imported' +# Global name for current design file +#EFABLESS PLATFORM +currdesign = '~/.open_pdks/currdesign' +prefsfile = '~/.open_pdks/prefs.json' + + +#--------------------------------------------------------------- +# Watch a directory for modified time change. Repeat every two +# seconds. Call routine callback() if a change occurs +#--------------------------------------------------------------- + +class WatchClock(object): + def __init__(self, parent, path, callback, interval=2000, interval0=None): + self.parent = parent + self.callback = callback + self.path = path + self.interval = interval + if interval0 != None: + self.interval0 = interval0 + self.restart(first=True) + else: + self.interval0 = interval + self.restart() + + def query(self): + for entry in self.path: + statbuf = os.stat(entry) + if statbuf.st_mtime > self.reftime: + self.callback() + self.restart() + return + self.timer = self.parent.after(self.interval, self.query) + + def stop(self): + self.parent.after_cancel(self.timer) + + # if first: optionally use different (typically shorter) interval, AND DON'T + # pre-record watched-dir mtime-s (which forces the callback on first timer fire) + def restart(self, first=False): + self.reftime = 0 + if not first: + for entry in self.path: + statbuf = os.stat(entry) + if statbuf.st_mtime > self.reftime: + self.reftime = statbuf.st_mtime + self.timer = self.parent.after(self.interval0 if first and self.interval0 != None else self.interval, self.query) + +#------------------------------------------------------ +# Dialog for generating a new layout +#------------------------------------------------------ + +class NewLayoutDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed=''): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + + self.l1prefs = tkinter.IntVar(master) + self.l1prefs.set(1) + ttk.Checkbutton(master, text='Populate new layout from netlist', + variable = self.l1prefs).grid(row = 2, columnspan = 2, sticky = 'enws') + + return self + + def apply(self): + return self.l1prefs.get + +#------------------------------------------------------ +# Simple dialog for entering project names +#------------------------------------------------------ + +class ProjectNameDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed=''): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + ttk.Label(master, text='Enter new project name:').grid(row = 1, column = 0, sticky = 'wns') + self.nentry = ttk.Entry(master) + self.nentry.grid(row = 1, column = 1, sticky = 'ewns') + self.nentry.insert(0, seed) + return self.nentry # initial focus + + def apply(self): + return self.nentry.get() + +class PadFrameCellNameDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed=''): + description='PadFrame' # TODO: make this an extra optional parameter of a generic CellNameDialog? + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + if description: + description = description + " " + else: + description = "" + ttk.Label(master, text=("Enter %scell name:" %(description))).grid(row = 1, column = 0, sticky = 'wns') + self.nentry = ttk.Entry(master) + self.nentry.grid(row = 1, column = 1, sticky = 'ewns') + self.nentry.insert(0, seed) + return self.nentry # initial focus + + def apply(self): + return self.nentry.get() + +#------------------------------------------------------ +# Dialog for copying projects. Includes checkbox +# entries for preferences. +#------------------------------------------------------ + +class CopyProjectDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed=''): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0, sticky = 'wns') + self.nentry = ttk.Entry(master) + self.nentry.grid(row = 1, column = 1, sticky = 'ewns') + self.nentry.insert(0, seed) + self.elprefs = tkinter.IntVar(master) + self.elprefs.set(0) + ttk.Checkbutton(master, text='Copy electric preferences (not recommended)', + variable = self.elprefs).grid(row = 2, columnspan = 2, sticky = 'enws') + self.spprefs = tkinter.IntVar(master) + self.spprefs.set(0) + ttk.Checkbutton(master, text='Copy ngspice folder (not recommended)', + variable = self.spprefs).grid(row = 3, columnspan = 2, sticky = 'enws') + return self.nentry # initial focus + + def apply(self): + # Return a list containing the entry text and the checkbox states. + elprefs = True if self.elprefs.get() == 1 else False + spprefs = True if self.spprefs.get() == 1 else False + return [self.nentry.get(), elprefs, spprefs] + +#------------------------------------------------------- +# Not-Quite-So-Simple dialog for entering a new project. +# Select a project name and a PDK from a drop-down list. +#------------------------------------------------------- + +class NewProjectDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed='', importnode=None, development=False): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0) + self.nentry = ttk.Entry(master) + self.nentry.grid(row = 1, column = 1, sticky = 'ewns') + self.nentry.insert(0, seed or '') # may be None + self.pvar = tkinter.StringVar(master) + if not importnode: + # Add PDKs as found by searching /ef/tech for 'libs.tech' directories + ttk.Label(master, text="Select foundry/node:").grid(row = 2, column = 0) + else: + ttk.Label(master, text="Foundry/node:").grid(row = 2, column = 0) + self.infolabel = ttk.Label(master, text="", style = 'brown.TLabel', wraplength=250) + self.infolabel.grid(row = 3, column = 0, columnspan = 2, sticky = 'news') + self.pdkmap = {} + self.pdkdesc = {} + self.pdkstat = {} + pdk_def = None + + node_def = importnode + if not node_def: + node_def = "EFXH035B" + + # use glob instead of os.walk. Don't need to recurse large PDK hier. + # TODO: stop hardwired default EFXH035B: get from an overall flow /ef/tech/.ef-config/plist.json + # (or get it from the currently selected project) + #EFABLESS PLATFORM + #TODO: Replace with PREFIX + for pdkdir_lr in glob.glob('/usr/share/pdk/*/libs.tech/'): + pdkdir = os.path.split( os.path.split( pdkdir_lr )[0])[0] # discard final .../libs.tech/ + (foundry, node, desc, status) = OpenGalaxyManager.pdkdir2fnd( pdkdir ) + if not foundry or not node: + continue + key = foundry + '/' + node + self.pdkmap[key] = pdkdir + self.pdkdesc[key] = desc + self.pdkstat[key] = status + if node == node_def and not pdk_def: + pdk_def = key + + # Quick hack: sorting puts EFXH035A before EFXH035LEGACY. However, some + # ranking is needed. + pdklist = sorted( self.pdkmap.keys()) + if not pdklist: + raise ValueError( "assertion failed, no available PDKs found") + pdk_def = (pdk_def or pdklist[0]) + + self.pvar.set(pdk_def) + + # Restrict list to single entry if importnode was non-NULL and + # is in the PDK list (OptionMenu is replaced by a simple label) + # Otherwise, restrict the list to entries having an "status" + # entry equal to "active". This allows some legacy PDKs to be + # disabled for creating new projects (but available for projects + # that already have them). + + if importnode: + self.pdkselect = ttk.Label(master, text = pdk_def, style='blue.TLabel') + else: + pdkactive = list(item for item in pdklist if self.pdkstat[item] == 'active') + if development: + pdkactive.extend(list(item for item in pdklist if self.pdkstat[item] == 'development')) + + self.pdkselect = ttk.OptionMenu(master, self.pvar, pdk_def, *pdkactive, + style='blue.TMenubutton', command=self.show_info) + self.pdkselect.grid(row = 2, column = 1) + self.show_info(0) + + return self.nentry # initial focus + + def show_info(self, args): + key = str(self.pvar.get()) + desc = self.pdkdesc[key] + if desc == '': + self.infolabel.config(text='(no description available)') + else: + self.infolabel.config(text=desc) + + def apply(self): + return self.nentry.get(), self.pdkmap[ str(self.pvar.get()) ] # Note converts StringVar to string + +#---------------------------------------------------------------- +# Not-Quite-So-Simple dialog for selecting an existing project. +# Select a project name from a drop-down list. This could be +# replaced by simply using the selected (current) project. +#---------------------------------------------------------------- + +class ExistingProjectDialog(tksimpledialog.Dialog): + def body(self, master, plist, seed, warning='Enter name of existing project to import into:'): + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + + # Alphebetize list + plist.sort() + # Add projects + self.pvar = tkinter.StringVar(master) + self.pvar.set(plist[0]) + + ttk.Label(master, text='Select project:').grid(row = 1, column = 0) + + self.projectselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton') + self.projectselect.grid(row = 1, column = 1, sticky = 'ewns') + # pack version (below) hangs. Don't know why, changed to grid (like ProjectNameDialog) + # self.projectselect.pack(side = 'top', fill = 'both', expand = 'true') + return self.projectselect # initial focus + + def apply(self): + return self.pvar.get() # Note converts StringVar to string + +#---------------------------------------------------------------- +# Not-Quite-So-Simple dialog for selecting an existing ElecLib of existing project. +# Select an elecLib name from a drop-down list. +#---------------------------------------------------------------- + +class ExistingElecLibDialog(tksimpledialog.Dialog): + def body(self, master, plist, seed): + warning = "Enter name of existing Electric library to import into:" + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + + # Alphebetize list + plist.sort() + # Add electric libraries + self.pvar = tkinter.StringVar(master) + self.pvar.set(plist[0]) + + ttk.Label(master, text="Select library:").grid(row = 1, column = 0) + + self.libselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton') + self.libselect.grid(row = 1, column = 1) + return self.libselect # initial focus + + def apply(self): + return self.pvar.get() # Note converts StringVar to string + +#---------------------------------------------------------------- +# Dialog for layout, in case of multiple layout names, none of +# which matches the project name (ip-name). Method: Select a +# layout name from a drop-down list. If there is no project.json +# file, add a checkbox for creating one and seeding the ip-name +# with the name of the selected layout. Include entry for +# new layout, and for new layouts add a checkbox to import the +# layout from schematic or verilog, if a valid candidate exists. +#---------------------------------------------------------------- + +class EditLayoutDialog(tksimpledialog.Dialog): + def body(self, master, plist, seed='', ppath='', pname='', warning='', hasnet=False): + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + self.ppath = ppath + self.pname = pname + + # Checkbox variable + self.confirm = tkinter.IntVar(master) + self.confirm.set(0) + + # To-Do: Add checkbox for netlist import + + # Alphebetize list + plist.sort() + # Add additional item for new layout + plist.append('(New layout)') + + # Add layouts to list + self.pvar = tkinter.StringVar(master) + self.pvar.set(plist[0]) + + ttk.Label(master, text='Selected layout to edit:').grid(row = 1, column = 0) + + if pname in plist: + pseed = plist.index(pname) + else: + pseed = 0 + + self.layoutselect = ttk.OptionMenu(master, self.pvar, plist[pseed], *plist, + style='blue.TMenubutton', command=self.handle_choice) + self.layoutselect.grid(row = 1, column = 1, sticky = 'ewns') + + # Create an entry form and checkbox for entering a new layout name, but + # keep them unpacked unless the "(New layout)" selection is chosen. + + self.layoutbox = ttk.Frame(master) + self.layoutlabel = ttk.Label(self.layoutbox, text='New layout name:') + self.layoutlabel.grid(row = 0, column = 0, sticky = 'ewns') + self.layoutentry = ttk.Entry(self.layoutbox) + self.layoutentry.grid(row = 0, column = 1, sticky = 'ewns') + self.layoutentry.insert(0, pname) + + # Only allow 'makeproject' checkbox if there is no project.json file + jname = ppath + '/project.json' + if not os.path.exists(jname): + dname = os.path.split(ppath)[1] + jname = ppath + '/' + dname + '.json' + if not os.path.exists(jname): + self.makeproject = ttk.Checkbutton(self.layoutbox, + text='Make default project name', + variable = self.confirm) + self.makeproject.grid(row = 2, column = 0, columnspan = 2, sticky = 'ewns') + return self.layoutselect # initial focus + + def handle_choice(self, event): + if self.pvar.get() == '(New layout)': + # Add entry and checkbox for creating ad-hoc project.json file + self.layoutbox.grid(row = 1, column = 0, columnspan = 2, sticky = 'ewns') + else: + # Remove entry and checkbox + self.layoutbox.grid_forget() + return + + def apply(self): + if self.pvar.get() == '(New layout)': + if self.confirm.get() == 1: + pname = self.pname + master.create_ad_hoc_json(self.layoutentry.get(), pname) + return self.layoutentry.get() + else: + return self.pvar.get() # Note converts StringVar to string + +#---------------------------------------------------------------- +# Dialog for padframe: select existing ElecLib of existing project, type in a cellName. +# Select an elecLib name from a drop-down list. +# Text field for entry of a cellName. +#---------------------------------------------------------------- + +class ExistingElecLibCellDialog(tksimpledialog.Dialog): + def body(self, master, descPre, seed='', descPost='', plist=None, seedLibNm=None, seedCellNm=''): + warning = 'Pick existing Electric library; enter cell name' + warning = (descPre or '') + ((descPre and ': ') or '') + warning + ((descPost and ' ') or '') + (descPost or '') + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + + # Alphebetize list + plist.sort() + # Add electric libraries + self.pvar = tkinter.StringVar(master) + pNdx = 0 + if seedLibNm and seedLibNm in plist: + pNdx = plist.index(seedLibNm) + self.pvar.set(plist[pNdx]) + + ttk.Label(master, text='Electric library:').grid(row = 1, column = 0, sticky = 'ens') + self.libselect = ttk.OptionMenu(master, self.pvar, plist[pNdx], *plist, style='blue.TMenubutton') + self.libselect.grid(row = 1, column = 1, sticky = 'wns') + + ttk.Label(master, text=('cell name:')).grid(row = 2, column = 0, sticky = 'ens') + self.nentry = ttk.Entry(master) + self.nentry.grid(row = 2, column = 1, sticky = 'ewns') + self.nentry.insert(0, seedCellNm) + + return self.libselect # initial focus + + def apply(self): + # return list of 2 strings: selected ElecLibName, typed-in cellName. + return [self.pvar.get(), self.nentry.get()] # Note converts StringVar to string + +#------------------------------------------------------ +# Simple dialog for confirming anything. +#------------------------------------------------------ + +class ConfirmDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + return self + + def apply(self): + return 'okay' + +#------------------------------------------------------ +# More proactive dialog for confirming an invasive +# procedure like "delete project". Requires user to +# click a checkbox to ensure this is not a mistake. +# confirmPrompt can be overridden, default='I am sure I want to do this.' +#------------------------------------------------------ + +class ProtectedConfirmDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed='', confirmPrompt=None): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + self.confirm = tkinter.IntVar(master) + self.confirm.set(0) + if not confirmPrompt: + confirmPrompt='I am sure I want to do this.' + ttk.Checkbutton(master, text=confirmPrompt, + variable = self.confirm).grid(row = 1, columnspan = 2, sticky = 'enws') + return self + + def apply(self): + return 'okay' if self.confirm.get() == 1 else '' + +#------------------------------------------------------ +# Simple dialog to say "blah is not implemented yet." +#------------------------------------------------------ + +class NotImplementedDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed): + if not warning: + warning = "Sorry, that feature is not implemented yet" + if warning: + warning = "Sorry, " + warning + ", is not implemented yet" + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + return self + + def apply(self): + return 'okay' + +#------------------------------------------------------ +# (This is actually a generic confirm dialogue, no install/overwrite intelligence) +# But so far dedicated to confirming the installation of one or more files, +# with notification of which (if any) will overwrite existing files. +# +# The warning parameter is fully constructed by caller, as multiple lines as either: +# For the import of module 'blah', +# CONFIRM installation of (*: OVERWRITE existing): +# * path1 +# path2 +# .... +# or: +# For the import of module 'blah', +# CONFIRM installation of: +# path1 +# path2 +# .... +# TODO: bastardizes warning parameter as multiple lines. Implement some other way? +#------------------------------------------------------ + +class ConfirmInstallDialog(tksimpledialog.Dialog): + def body(self, master, warning, seed): + if warning: + ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') + return self + + def apply(self): + return 'okay' + +#------------------------------------------------------ +# Open Galaxy Manager class +#------------------------------------------------------ + +class OpenGalaxyManager(ttk.Frame): + """Open Galaxy Project Management GUI.""" + + def __init__(self, parent, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.root = parent + parent.withdraw() + # self.update() + self.update_idletasks() # erase small initial frame asap + self.init_gui() + parent.protocol("WM_DELETE_WINDOW", self.on_quit) + if splash: + splash.destroy() + parent.deiconify() + + def on_quit(self): + """Exits program.""" + quit() + + def init_gui(self): + """Builds GUI.""" + global designdir + global importdir + global archiveimportdir + global currdesign + global theProg + global deferLoad + + message = [] + allPaneOpen = False + prjPaneMinh = 10 + iplPaneMinh = 4 + impPaneMinh = 4 + + # if deferLoad: # temp. for testing... open all panes + # allPaneOpen = True + + # Read user preferences + self.prefs = {} + self.read_prefs() + + # Get default font size from user preferences + fontsize = self.prefs['fontsize'] + + s = ttk.Style() + available_themes = s.theme_names() + # print("themes: " + str(available_themes)) + s.theme_use(available_themes[0]) + + s.configure('gray.TFrame', background='gray40') + s.configure('blue_white.TFrame', bordercolor = 'blue', borderwidth = 3) + s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic')) + s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'), + foreground = 'brown', anchor = 'center') + s.configure('title2.TLabel', font=('Helvetica', fontsize, 'bold italic'), + foreground = 'blue') + s.configure('normal.TLabel', font=('Helvetica', fontsize)) + s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red') + s.configure('brown.TLabel', font=('Helvetica', fontsize), foreground = 'brown3', background = 'gray95') + s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3') + s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue') + s.configure('normal.TButton', font=('Helvetica', fontsize), border = 3, relief = 'raised') + s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red', border = 3, + relief = 'raised') + s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3', border = 3, + relief = 'raised') + s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue', border = 3, + relief = 'raised') + + # Create the help window + self.help = HelpWindow(self, fontsize=fontsize) + + with io.StringIO() as buf, contextlib.redirect_stdout(buf): + self.help.add_pages_from_file(config.apps_path + '/manager_help.txt') + message = buf.getvalue() + + + # Set the help display to the first page + self.help.page(0) + + # Create the profile settings window + self.profile = Profile(self, fontsize=fontsize) + + # Variables used by option menus + self.seltype = tkinter.StringVar(self) + self.cur_project = tkinter.StringVar(self) + self.cur_import = "(nothing selected)" + self.project_name = "" + + # Root window title + self.root.title('Open Galaxy Project Manager') + self.root.option_add('*tearOff', 'FALSE') + self.pack(side = 'top', fill = 'both', expand = 'true') + + pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6) + pane.pack(side = 'top', fill = 'both', expand = 'true') + self.toppane = ttk.Frame(pane) + self.botpane = ttk.Frame(pane) + + # All interior windows size to toppane + self.toppane.columnconfigure(0, weight = 1) + # Projects window resizes preferably to others + self.toppane.rowconfigure(3, weight = 1) + + # Get username, and from it determine the project directory. + # Save this path, because it gets used often. + username = self.prefs['username'] + self.projectdir = os.path.expanduser('~/' + designdir) + self.cloudvdir = os.path.expanduser('~/' + cloudvdir) + + # Check that the project directory exists, and create it if not + if not os.path.isdir(self.projectdir): + os.makedirs(self.projectdir) + + # Label with the user + self.toppane.user_frame = ttk.Frame(self.toppane) + self.toppane.user_frame.grid(row = 0, sticky = 'news') + + # Put logo image in corner. Ignore if something goes wrong, as this + # is only decorative. Note: ef_logo must be kept as a record in self, + # or else it gets garbage collected. + try: + #EFABLESS PLATFORM + self.ef_logo = tkinter.PhotoImage(file='/ef/efabless/opengalaxy/efabless_logo_small.gif') + self.toppane.user_frame.logo = ttk.Label(self.toppane.user_frame, image=self.ef_logo) + self.toppane.user_frame.logo.pack(side = 'left', padx = 5) + except: + pass + + self.toppane.user_frame.title = ttk.Label(self.toppane.user_frame, text='User:', style='red.TLabel') + self.toppane.user_frame.user = ttk.Label(self.toppane.user_frame, text=username, style='blue.TLabel') + + self.toppane.user_frame.title.pack(side = 'left', padx = 5) + self.toppane.user_frame.user.pack(side = 'left', padx = 5) + + #--------------------------------------------- + ttk.Separator(self.toppane, orient='horizontal').grid(row = 1, sticky = 'news') + #--------------------------------------------- + + # List of projects: + self.toppane.design_frame = ttk.Frame(self.toppane) + self.toppane.design_frame.grid(row = 2, sticky = 'news') + + self.toppane.design_frame.design_header = ttk.Label(self.toppane.design_frame, text='Projects', + style='title.TLabel') + self.toppane.design_frame.design_header.pack(side = 'left', padx = 5) + + self.toppane.design_frame.design_header2 = ttk.Label(self.toppane.design_frame, + text='(' + self.projectdir + '/)', style='normal.TLabel') + self.toppane.design_frame.design_header2.pack(side = 'left', padx = 5) + + # Get current project from ~/.efmeta/currdesign and set the selection. + try: + with open(os.path.expanduser(currdesign), 'r') as f: + pnameCur = f.read().rstrip() + except: + pnameCur = None + + # Create listbox of projects + projectlist = self.get_project_list() if not deferLoad else [] + height = min(10, max(prjPaneMinh, 2 + len(projectlist))) + self.projectselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, selectVal=pnameCur, natSort=True) + self.projectselect.populate("Available Projects:", projectlist, + [["Create", True, self.createproject], + ["Copy", False, self.copyproject], + ["Rename IP", False, self.renameproject], + ["<CloudV", True, self.cloudvimport], + ["Clean", False, self.cleanproject], + ["Delete", False, self.deleteproject]], + height=height, columns=[0, 1]) + self.projectselect.grid(row = 3, sticky = 'news') + self.projectselect.bindselect(self.setcurrent) + + tooltip.ToolTip(self.projectselect.get_button(0), text="Create a new project") + tooltip.ToolTip(self.projectselect.get_button(1), text="Make a copy of an entire project") + tooltip.ToolTip(self.projectselect.get_button(2), text="Rename a project folder") + tooltip.ToolTip(self.projectselect.get_button(3), text="Import CloudV project as new project") + tooltip.ToolTip(self.projectselect.get_button(4), text="Clean simulation data from project") + tooltip.ToolTip(self.projectselect.get_button(5), text="Delete an entire project") + + pdklist = self.get_pdk_list(projectlist) + self.projectselect.populate2("PDK", projectlist, pdklist) + + if pnameCur: + try: + curitem = next(item for item in projectlist if pnameCur == os.path.split(item)[1]) + except StopIteration: + pass + else: + if curitem: + self.projectselect.setselect(pnameCur) + + # Check that the import directory exists, and create it if not + if not os.path.isdir(self.projectdir + '/' + importdir): + os.makedirs(self.projectdir + '/' + importdir) + + # Create a watchdog on the project and import directories + watchlist = [self.projectdir, self.projectdir + '/' + importdir] + if os.path.isdir(self.projectdir + '/upload'): + watchlist.append(self.projectdir + '/upload') + + # Check the creation time of the project manager app itself. Because the project + # manager tends to be left running indefinitely, it is important to know when it + # has been updated. This is checked once every hour since it is really expected + # only to happen occasionally. + + thisapp = [theProg] + self.watchself = WatchClock(self, thisapp, self.update_alert, 3600000) + + #--------------------------------------------- + + # Add second button bar for major project applications + self.toppane.apptitle = ttk.Label(self.toppane, text='Tools:', style='title2.TLabel') + self.toppane.apptitle.grid(row = 4, sticky = 'news') + self.toppane.appbar = ttk.Frame(self.toppane) + self.toppane.appbar.grid(row = 5, sticky = 'news') + + # Define the application buttons and actions + self.toppane.appbar.schem_button = ttk.Button(self.toppane.appbar, text='Edit Schematic', + command=self.edit_schematic, style = 'normal.TButton') + self.toppane.appbar.schem_button.pack(side = 'left', padx = 5) + self.toppane.appbar.layout_button = ttk.Button(self.toppane.appbar, text='Edit Layout', + command=self.edit_layout, style = 'normal.TButton') + self.toppane.appbar.layout_button.pack(side = 'left', padx = 5) + self.toppane.appbar.lvs_button = ttk.Button(self.toppane.appbar, text='Run LVS', + command=self.run_lvs, style = 'normal.TButton') + self.toppane.appbar.lvs_button.pack(side = 'left', padx = 5) + self.toppane.appbar.char_button = ttk.Button(self.toppane.appbar, text='Characterize', + command=self.characterize, style = 'normal.TButton') + self.toppane.appbar.char_button.pack(side = 'left', padx = 5) + self.toppane.appbar.synth_button = ttk.Button(self.toppane.appbar, text='Synthesis Flow', + command=self.synthesize, style = 'normal.TButton') + self.toppane.appbar.synth_button.pack(side = 'left', padx = 5) + + self.toppane.appbar.padframeCalc_button = ttk.Button(self.toppane.appbar, text='Pad Frame', + command=self.padframe_calc, style = 'normal.TButton') + self.toppane.appbar.padframeCalc_button.pack(side = 'left', padx = 5) + ''' + if self.prefs['schemeditor'] == 'xcircuit': + tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor") + elif self.prefs['schemeditor'] == 'xschem': + tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor") + else: + tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor") + + if self.prefs['layouteditor'] == 'klayout': + tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor") + else: + tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor") + ''' + self.refreshToolTips() + + tooltip.ToolTip(self.toppane.appbar.lvs_button, text="Start LVS tool") + tooltip.ToolTip(self.toppane.appbar.char_button, text="Start Characterization tool") + tooltip.ToolTip(self.toppane.appbar.synth_button, text="Start Digital Synthesis tool") + tooltip.ToolTip(self.toppane.appbar.padframeCalc_button, text="Start Pad Frame Generator") + + #--------------------------------------------- + ttk.Separator(self.toppane, orient='horizontal').grid(row = 6, sticky = 'news') + #--------------------------------------------- + # List of IP libraries: + self.toppane.library_frame = ttk.Frame(self.toppane) + self.toppane.library_frame.grid(row = 7, sticky = 'news') + + self.toppane.library_frame.library_header = ttk.Label(self.toppane.library_frame, text='IP Library:', + style='title.TLabel') + self.toppane.library_frame.library_header.pack(side = 'left', padx = 5) + + self.toppane.library_frame.library_header2 = ttk.Label(self.toppane.library_frame, + text='(' + self.projectdir + '/ip/)', style='normal.TLabel') + self.toppane.library_frame.library_header2.pack(side = 'left', padx = 5) + + self.toppane.library_frame.library_header3 = ttk.Button(self.toppane.library_frame, + text=(allPaneOpen and '-' or '+'), command=self.library_toggle, style = 'normal.TButton', width = 2) + self.toppane.library_frame.library_header3.pack(side = 'right', padx = 5) + + # Create listbox of IP libraries + iplist = self.get_library_list() if not deferLoad else [] + height = min(8, max(iplPaneMinh, 2 + len(iplist))) + self.ipselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, natSort=True) + self.ipselect.populate("IP Library:", iplist, + [], height=height, columns=[0, 1], versioning=True) + valuelist = self.ipselect.getvaluelist() + datelist = self.get_date_list(valuelist) + itemlist = self.ipselect.getlist() + self.ipselect.populate2("date", itemlist, datelist) + if allPaneOpen: + self.library_open() + + #--------------------------------------------- + ttk.Separator(self.toppane, orient='horizontal').grid(row = 9, sticky = 'news') + #--------------------------------------------- + # List of imports: + self.toppane.import_frame = ttk.Frame(self.toppane) + self.toppane.import_frame.grid(row = 10, sticky = 'news') + + self.toppane.import_frame.import_header = ttk.Label(self.toppane.import_frame, text='Imports:', + style='title.TLabel') + self.toppane.import_frame.import_header.pack(side = 'left', padx = 5) + + self.toppane.import_frame.import_header2 = ttk.Label(self.toppane.import_frame, + text='(' + self.projectdir + '/import/)', style='normal.TLabel') + self.toppane.import_frame.import_header2.pack(side = 'left', padx = 5) + + self.toppane.import_frame.import_header3 = ttk.Button(self.toppane.import_frame, + text=(allPaneOpen and '-' or '+'), command=self.import_toggle, style = 'normal.TButton', width = 2) + self.toppane.import_frame.import_header3.pack(side = 'right', padx = 5) + + # Create listbox of imports + importlist = self.get_import_list() if not deferLoad else [] + self.number_of_imports = len(importlist) if not deferLoad else None + height = min(8, max(impPaneMinh, 2 + len(importlist))) + self.importselect = TreeViewChoice(self.toppane, fontsize=fontsize, markDir=True, deferLoad=deferLoad) + self.importselect.populate("Pending Imports:", importlist, + [["Import As", False, self.importdesign], + ["Import Into", False, self.importintodesign], + ["Delete", False, self.deleteimport]], height=height, columns=[0, 1]) + valuelist = self.importselect.getvaluelist() + datelist = self.get_date_list(valuelist) + itemlist = self.importselect.getlist() + self.importselect.populate2("date", itemlist, datelist) + + tooltip.ToolTip(self.importselect.get_button(0), text="Import as a new project") + tooltip.ToolTip(self.importselect.get_button(1), text="Import into an existing project") + tooltip.ToolTip(self.importselect.get_button(2), text="Remove the import file(s)") + if allPaneOpen: + self.import_open() + + #--------------------------------------------- + # ttk.Separator(self, orient='horizontal').grid(column = 0, row = 8, columnspan=4, sticky='ew') + #--------------------------------------------- + + # Add a text window below the import to capture output. Redirect + # print statements to it. + self.botpane.console = ttk.Frame(self.botpane) + self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true') + + self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4) + self.text_box.pack(side='left', fill='both', expand = 'true') + console_scrollbar = ttk.Scrollbar(self.botpane.console) + console_scrollbar.pack(side='right', fill='y') + # attach console to scrollbar + self.text_box.config(yscrollcommand = console_scrollbar.set) + console_scrollbar.config(command = self.text_box.yview) + + # Give all the expansion weight to the message window. + # self.rowconfigure(9, weight = 1) + # self.columnconfigure(0, weight = 1) + + # at bottom (legacy mode): window height grows by one row. + # at top the buttons share a row with user name, reduce window height, save screen real estate. + bottomButtons = False + + # Add button bar: at the bottom of window (legacy mode), or share top row with user-name + if bottomButtons: + bbar = ttk.Frame(self.botpane) + bbar.pack(side='top', fill = 'x') + else: + bbar = self.toppane.user_frame + + # Define help button + bbar.help_button = ttk.Button(bbar, text='Help', + command=self.help.open, style = 'normal.TButton') + + # Define profile settings button + bbar.profile_button = ttk.Button(bbar, text='Settings', + command=self.profile.open, style = 'normal.TButton') + + # Define the "quit" button and action + bbar.quit_button = ttk.Button(bbar, text='Quit', command=self.on_quit, + style = 'normal.TButton') + # Tool tips for button bar + tooltip.ToolTip(bbar.quit_button, text="Exit the project manager") + tooltip.ToolTip(bbar.help_button, text="Show help window") + + if bottomButtons: + bbar.help_button.pack(side = 'left', padx = 5) + bbar.profile_button.pack(side = 'left', padx = 5) + bbar.quit_button.pack(side = 'right', padx = 5) + else: + # quit at TR like window-title's close; help towards the outside, settings towards inside + bbar.quit_button.pack(side = 'right', padx = 5) + bbar.help_button.pack(side = 'right', padx = 5) + bbar.profile_button.pack(side = 'right', padx = 5) + + # Add the panes once the internal geometry is known + pane.add(self.toppane) + pane.add(self.botpane) + pane.paneconfig(self.toppane, stretch='first') + # self.update_idletasks() + + #--------------------------------------------------------------- + # Project list + # projects = os.listdir(os.path.expanduser('~/' + designdir)) + # self.cur_project.set(projects[0]) + # self.design_select = ttk.OptionMenu(self, self.cur_project, projects[0], *projects, + # style='blue.TMenubutton') + + # New import list + # self.import_select = ttk.Button(self, text=self.cur_import, command=self.choose_import) + + #--------------------------------------------------------- + # Define project design actions + # self.design_actions = ttk.Frame(self) + # self.design_actions.characterize = ttk.Button(self.design_actions, + # text='Upload and Characterize', command=self.characterize) + # self.design_actions.characterize.grid(column = 0, row = 0) + + # Define import actions + # self.import_actions = ttk.Frame(self) + # self.import_actions.upload = ttk.Button(self.import_actions, + # text='Upload Challenge', command=self.make_challenge) + # self.import_actions.upload.grid(column = 0, row = 0) + + self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000, + 0 if deferLoad else None) # do immediate forced refresh (1st in mainloop) + # self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000) + + # Redirect stdout and stderr to the console as the last thing to do. . . + # Otherwise errors in the GUI get sucked into the void. + self.stdout = sys.stdout + self.stderr = sys.stderr + sys.stdout = ConsoleText.StdoutRedirector(self.text_box) + sys.stderr = ConsoleText.StderrRedirector(self.text_box) + + if message: + print(message) + + if self.prefs == {}: + print("No user preferences file, using default settings.") + + # helper for Profile to do live mods of some of the user-prefs (without restart projectManager): + def setUsername(self, newname): + self.toppane.user_frame.user.config(text=newname) + + def refreshToolTips(self): + if self.prefs['schemeditor'] == 'xcircuit': + tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor") + elif self.prefs['schemeditor'] == 'xschem': + tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor") + else: + tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor") + + if self.prefs['layouteditor'] == 'klayout': + tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor") + else: + tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor") + + def config_path(self, path): + #returns the directory that path contains between .config and .ef-config + if (os.path.exists(path + '/.config')): + return '/.config' + elif (os.path.exists(path + '/.ef-config')): + return '/.ef-config' + raise FileNotFoundError('Neither '+path+'/.config nor '+path+'/.ef-config exists.') + + #------------------------------------------------------------------------ + # Check if a name is blacklisted for being a project folder + #------------------------------------------------------------------------ + + def blacklisted(self, dirname): + # Blacklist: Do not show files of these names: + blacklist = [importdir, 'ip', 'upload', 'export', 'lost+found'] + if dirname in blacklist: + return True + else: + return False + + def write_prefs(self): + global prefsfile + + if self.prefs: + expprefsfile = os.path.expanduser(prefsfile) + prefspath = os.path.split(expprefsfile)[0] + if not os.path.exists(prefspath): + os.makedirs(prefspath) + with open(os.path.expanduser(prefsfile), 'w') as f: + json.dump(self.prefs, f, indent = 4) + + def read_prefs(self): + global prefsfile + + # Set all known defaults even if they are not in the JSON file so + # that it is not necessary to check for the existence of the keyword + # in the dictionary every time it is accessed. + if 'fontsize' not in self.prefs: + self.prefs['fontsize'] = 11 + userid = os.environ['USER'] + uid = '' + username = userid + self.prefs['username'] = username + + ''' + if 'username' not in self.prefs: + + # + #EFABLESS PLATFORM + p = subprocess.run(['/ef/apps/bin/withnet' , + og_config.apps_path + '/og_uid_service.py', userid], + stdout = subprocess.PIPE) + if p.stdout: + uid_string = p.stdout.splitlines()[0].decode('utf-8') + userspec = re.findall(r'[^"\s]\S*|".+?"', uid_string) + if len(userspec) > 0: + username = userspec[0].strip('"') + # uid = userspec[1] + # Note userspec[1] = UID and userspec[2] = role, useful + # for future applications. + else: + username = userid + else: + username = userid + self.prefs['username'] = username + # self.prefs['uid'] = uid + ''' + if 'schemeditor' not in self.prefs: + self.prefs['schemeditor'] = 'electric' + + if 'layouteditor' not in self.prefs: + self.prefs['layouteditor'] = 'magic' + + if 'magic-graphics' not in self.prefs: + self.prefs['magic-graphics'] = 'X11' + + if 'development' not in self.prefs: + self.prefs['development'] = False + + if 'devstdcells' not in self.prefs: + self.prefs['devstdcells'] = False + + # Any additional user preferences go above this line. + + # Get user preferences from ~/design/.profile/prefs.json and use it to + # overwrite default entries in self.prefs + try: + with open(os.path.expanduser(prefsfile), 'r') as f: + prefsdict = json.load(f) + for key in prefsdict: + self.prefs[key] = prefsdict[key] + except: + # No preferences file, so create an initial one. + if not os.path.exists(prefsfile): + self.write_prefs() + + # if 'User:' Label exists, this updates it live (Profile calls read_prefs after write) + try: + self.setUsername(self.prefs['username']) + except: + pass + + #------------------------------------------------------------------------ + # Get a list of the projects in the user's design directory. Exclude + # items that are not directories, or which are blacklisted. + #------------------------------------------------------------------------ + + def get_project_list(self): + global importdir + + badrex1 = re.compile("^\.") + badrex2 = re.compile(".*[ \t\n].*") + + # Get contents of directory. Look only at directories + projectlist = list(item for item in os.listdir(self.projectdir) if + os.path.isdir(self.projectdir + '/' + item)) + + # 'import' and others in the blacklist are not projects! + # Files beginning with '.' and files with whitespace are + # also not listed. + for item in projectlist[:]: + if self.blacklisted(item): + projectlist.remove(item) + elif badrex1.match(item): + projectlist.remove(item) + elif badrex2.match(item): + projectlist.remove(item) + + # Add pathname to all items in projectlist + projectlist = [self.projectdir + '/' + item for item in projectlist] + return projectlist + + #------------------------------------------------------------------------ + # Get a list of the projects in the user's cloudv directory. Exclude + # items that are not directories, or which are blacklisted. + #------------------------------------------------------------------------ + + def get_cloudv_project_list(self): + global importdir + + badrex1 = re.compile("^\.") + badrex2 = re.compile(".*[ \t\n].*") + + if not os.path.exists(self.cloudvdir): + print('No user cloudv dir exists; no projects to import.') + return None + + # Get contents of cloudv directory. Look only at directories + projectlist = list(item for item in os.listdir(self.cloudvdir) if + os.path.isdir(self.cloudvdir + '/' + item)) + + # 'import' and others in the blacklist are not projects! + # Files beginning with '.' and files with whitespace are + # also not listed. + for item in projectlist[:]: + if self.blacklisted(item): + projectlist.remove(item) + elif badrex1.match(item): + projectlist.remove(item) + elif badrex2.match(item): + projectlist.remove(item) + + # Add pathname to all items in projectlist + projectlist = [self.cloudvdir + '/' + item for item in projectlist] + return projectlist + + #------------------------------------------------------------------------ + # utility: [re]intialize a project's elec/ dir: the .java preferences and LIBDIRS. + # So user can just delete .java, and restart electric (from projectManager), to reinit preferences. + # So user can just delete LIBDIRS, and restart electric (from projectManager), to reinit LIBDIRS. + # So project copies/imports can filter ngspice/run (and ../.allwaves), we'll recreate it here. + # + # The global /ef/efabless/deskel/* is used and the PDK name substituted. + # + # This SINGLE function is used to setup elec/ contents for new projects, in addition to being + # called in-line prior to "Edit Schematics" (on-the-fly). + #------------------------------------------------------------------------ + @classmethod + def reinitElec(cls, design): + pdkdir = os.path.join( design, ".ef-config/techdir") + elec = os.path.join( design, "elec") + + # on the fly, ensure has elec/ dir, ensure has ngspice/run/allwaves dir + try: + os.makedirs(design + '/elec', exist_ok=True) + except IOError as e: + print('Error in os.makedirs(elec): ' + str(e)) + try: + os.makedirs(design + '/ngspice/run/.allwaves', exist_ok=True) + except IOError as e: + print('Error in os.makedirs(.../.allwaves): ' + str(e)) + #EFABLESS PLATFORM + deskel = '/ef/efabless/deskel' + + # on the fly: + # .../elec/.java : reinstall if missing. From PDK-specific if any. + if not os.path.exists( os.path.join( elec, '.java')): + # Copy Electric preferences + try: + shutil.copytree(deskel + '/dotjava', design + '/elec/.java', symlinks = True) + except IOError as e: + print('Error copying files: ' + str(e)) + + # .../elec/LIBDIRS : reinstall if missing, from PDK-specific LIBDIRS + # in libs.tech/elec/LIBDIRS + + libdirsloc = pdkdir + '/libs.tech/elec/LIBDIRS' + + if not os.path.exists( os.path.join( elec, 'LIBDIRS')): + if os.path.exists( libdirsloc ): + # Copy Electric LIBDIRS + try: + shutil.copy(libdirsloc, design + '/elec/LIBDIRS') + except IOError as e: + print('Error copying files: ' + str(e)) + else: + print('Info: PDK not configured for Electric: no libs.tech/elec/LIBDIRS') + + return None + + #------------------------------------------------------------------------ + # utility: filter a list removing: empty strings, strings with any whitespace + #------------------------------------------------------------------------ + whitespaceREX = re.compile('\s') + @classmethod + def filterNullOrWS(cls, inlist): + return [ i for i in inlist if i and not cls.whitespaceREX.search(i) ] + + #------------------------------------------------------------------------ + # utility: do a glob.glob of relative pattern, but specify the rootDir, + # so returns the matching paths found below that rootDir. + #------------------------------------------------------------------------ + @classmethod + def globFromDir(cls, pattern, dir=None): + if dir: + dir = dir.rstrip('/') + '/' + pattern = dir + pattern + result = glob.glob(pattern) + if dir and result: + nbr = len(dir) + result = [ i[nbr:] for i in result ] + return result + + #------------------------------------------------------------------------ + # utility: from a pdkPath, return list of 3 strings: <foundry>, <node>, <description>. + # i.e. pdkPath has form '[.../]<foundry>[.<ext>]/<node>'. For now the description + # is always ''. And an optional foundry extension is pruned/dropped. + # thus '.../XFAB.2/EFXP018A4' -> 'XFAB', 'EFXP018A4', '' + # + # optionally store in each PDK: .ef-config/nodeinfo.json which can define keys: + # 'foundry', 'node', 'description' to override the foundry (computed from the path) + # and (fixed, empty) description currently returned by this. + # + # Intent: keep a short-description field at least, intended to be one-line max 40 chars, + # suitable for a on-hover-tooltip display. (Distinct from a big multiline description). + # + # On error (malformed pdkPath: can't determine foundry or node), the foundry or node + # or both may be '' or as specified in the optional default values (if you're + # generating something for display and want an unknown to appear as 'none' etc.). + #------------------------------------------------------------------------ + @classmethod + def pdkdir2fnd(cls, pdkdir, def_foundry='', def_node='', def_description=''): + foundry = '' + node = '' + description = '' + status = 'active' + if pdkdir: + split = os.path.split(os.path.realpath(pdkdir)) + # Full path should be [<something>/]<foundry>[.ext]/<node> + node = split[1] + foundry = os.path.split(split[0])[1] + foundry = os.path.splitext(foundry)[0] + # Check for nodeinfo.json + infofile = pdkdir + '/.config/nodeinfo.json' + if os.path.exists(infofile): + with open(infofile, 'r') as ifile: + nodeinfo = json.load(ifile) + if 'foundry' in nodeinfo: + foundry = nodeinfo['foundry'] + if 'node' in nodeinfo: + node = nodeinfo['node'] + if 'description' in nodeinfo: + description = nodeinfo['description'] + if 'status' in nodeinfo: + status = nodeinfo['status'] + return foundry, node, description, status + + infofile = pdkdir + '/.ef-config/nodeinfo.json' + if os.path.exists(infofile): + with open(infofile, 'r') as ifile: + nodeinfo = json.load(ifile) + if 'foundry' in nodeinfo: + foundry = nodeinfo['foundry'] + if 'node' in nodeinfo: + node = nodeinfo['node'] + if 'description' in nodeinfo: + description = nodeinfo['description'] + if 'status' in nodeinfo: + status = nodeinfo['status'] + + + return foundry, node, description, status + + #------------------------------------------------------------------------ + # Get a list of the electric-libraries (DELIB only) in a given project. + # List of full-paths each ending in '.delib' + #------------------------------------------------------------------------ + + def get_elecLib_list(self, pname): + elibs = self.globFromDir(pname + '/elec/*.delib/', self.projectdir) + elibs = [ re.sub("/$", "", i) for i in elibs ] + return self.filterNullOrWS(elibs) + + #------------------------------------------------------------------------ + # Create a list of datestamps for each import file + #------------------------------------------------------------------------ + def get_date_list(self, valuelist): + datelist = [] + for value in valuelist: + try: + importfile = value[0] + try: + statbuf = os.stat(importfile) + except: + # Note entries that can't be accessed. + datelist.append("(unknown)") + else: + datestamp = datetime.datetime.fromtimestamp(statbuf.st_mtime) + datestr = datestamp.strftime("%c") + datelist.append(datestr) + except: + datelist.append("(N/A)") + + return datelist + + #------------------------------------------------------------------------ + # Get the PDK attached to a project for display as: '<foundry> : <node>' + # unless path=True: then return true PDK dir-path. + # + # TODO: the ef-config prog output is not used below. Intent was use + # ef-config to be the one official query for *any* project's PDK value, and + # therein-only hide a built-in default for legacy projects without techdir symlink. + # In below ef-config will always give an EF_TECHDIR, so that code-branch always + # says '(default)', the ef-config subproc is wasted, and '(no PDK)' is never + # reached. + #------------------------------------------------------------------------ + def get_pdk_dir(self, project, path=False): + pdkdir = os.path.realpath(project + self.config_path(project)+'/techdir') + if path: + return pdkdir + foundry, node, desc, status = self.pdkdir2fnd( pdkdir ) + return foundry + ' : ' + node + ''' + if os.path.isdir(project + '/.ef-config'): + if os.path.exists(project + '/.ef-config/techdir'): + pdkdir = os.path.realpath(project + '/.ef-config/techdir') + + elif os.path.isdir(project + '/.config'): + if os.path.exists(project + '/.config/techdir'): + pdkdir = os.path.realpath(project + '/.config/techdir') + if path: + return pdkdir + foundry, node, desc, status = self.pdkdir2fnd( pdkdir ) + return foundry + ' : ' + node + ''' + ''' + if not pdkdir: + # Run "ef-config" script for backward compatibility + export = {'EF_DESIGNDIR': project} + #EFABLESS PLATFORM + p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'], + stdout = subprocess.PIPE, env = export) + config_out = p.stdout.splitlines() + for line in config_out: + setline = line.decode('utf-8').split('=') + if setline[0] == 'EF_TECHDIR': + pdkdir = ( setline[1] if path else '(default)' ) + if not pdkdir: + pdkdir = ( None if path else '(no PDK)' ) # shouldn't get here + ''' + + + + return pdkdir + + #------------------------------------------------------------------------ + # Get the list of PDKs that are attached to each project + #------------------------------------------------------------------------ + def get_pdk_list(self, projectlist): + pdklist = [] + for project in projectlist: + pdkdir = self.get_pdk_dir(project) + pdklist.append(pdkdir) + + return pdklist + + #------------------------------------------------------------------------ + # Find a .json's associated tar.gz (or .tgz) if any. + # Return path to the tar.gz if any, else None. + #------------------------------------------------------------------------ + + def json2targz(self, jsonPath): + root = os.path.splitext(jsonPath)[0] + for ext in ('.tgz', '.tar.gz'): + if os.path.isfile(root + ext): + return root + ext + return None + + #------------------------------------------------------------------------ + # Remove a .json and associated tar.gz (or .tgz) if any. + # If not a .json, remove just that file (no test for a tar). + #------------------------------------------------------------------------ + + def removeJsonPlus(self, jsonPath): + ext = os.path.splitext(jsonPath)[1] + if ext == ".json": + tar = self.json2targz(jsonPath) + if tar: os.remove(tar) + return os.remove(jsonPath) + + #------------------------------------------------------------------------ + # MOVE a .json and associated tar.gz (or .tgz) if any, to targetDir. + # If not a .json, move just that file (no test for a tar). + #------------------------------------------------------------------------ + + def moveJsonPlus(self, jsonPath, targetDir): + ext = os.path.splitext(jsonPath)[1] + if ext == ".json": + tar = self.json2targz(jsonPath) + if tar: + shutil.move(tar, targetDir) + # believe the move throws an error. So return value (the targetDir name) isn't really useful. + return shutil.move(jsonPath, targetDir) + + #------------------------------------------------------------------------ + # Get a list of the libraries in the user's ip folder + #------------------------------------------------------------------------ + + def get_library_list(self): + # Get contents of directory + try: + iplist = glob.glob(self.projectdir + '/ip/*/*') + except: + iplist = [] + else: + pass + + return iplist + + #------------------------------------------------------------------------ + # Get a list of the files in the user's design import folder + # (use current 'import' but also original 'upload') + #------------------------------------------------------------------------ + + def get_import_list(self): + # Get contents of directory + importlist = os.listdir(self.projectdir + '/' + importdir) + + # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz). + # Also ignore any .swp files dropped by the vim editor. + # Also ignore any subdirectories of import + for item in importlist[:]: + if item[-1] in '#~': + importlist.remove(item) + continue + ipath = self.projectdir + '/' + importdir + '/' + item + + # recognize dirs (as u2u projects) if not symlink and has a 'project.json', + # hide dirs named *.bak. If originating user does u2u twice before target user + # can consume/import it, the previous one (only) is retained as *.bak. + if os.path.isdir(ipath): + if os.path.islink(ipath) or not self.validProjectName(item) \ + or self.importProjNameBadrex1.match(item) \ + or not os.path.isfile(ipath + '/project.json'): + importlist.remove(item) + continue + else: + ext = os.path.splitext(item) + if ext[1] == '.json': + if ext[0] + '.tar.gz' in importlist: + importlist.remove(ext[0] + '.tar.gz') + elif ext[0] + '.tgz' in importlist: + importlist.remove(ext[0] + '.tgz') + elif ext[1] == '.swp': + importlist.remove(item) + elif os.path.isdir(self.projectdir + '/' + importdir + '/' + item): + importlist.remove(item) + + # Add pathname to all items in projectlist + importlist = [self.projectdir + '/' + importdir + '/' + item for item in importlist] + + # Add support for original "upload" directory (backward compatibility) + if os.path.exists(self.projectdir + '/upload'): + uploadlist = os.listdir(self.projectdir + '/upload') + + # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz). + # Also ignore any .swp files dropped by the vim editor. + for item in uploadlist[:]: + ext = os.path.splitext(item) + if ext[1] == '.json': + if ext[0] + '.tar.gz' in uploadlist: + uploadlist.remove(ext[0] + '.tar.gz') + elif ext[0] + '.tgz' in uploadlist: + uploadlist.remove(ext[0] + '.tgz') + elif ext[1] == '.swp': + uploadlist.remove(item) + + # Add pathname to all items in projectlist + uploadlist = [self.projectdir + '/upload/' + item for item in uploadlist] + importlist.extend(uploadlist) + + # Remember the size of the list so we know when it changed + self.number_of_imports = len(importlist) + return importlist + + #------------------------------------------------------------------------ + # Import for json documents and related tarballs (.gz or .tgz): + #------------------------------------------------------------------------ + + def importjson(self, projname, importfile): + # (1) Check if there is a tarball with the same root name as the JSON + importroot = os.path.splitext(importfile)[0] + badrex1 = re.compile("^\.") + badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*") + if os.path.isfile(importroot + '.tgz'): + tarname = importroot + '.tgz' + elif os.path.isfile(importroot + '.tar.gz'): + tarname = importroot + '.tar.gz' + else: + tarname = [] + # (2) Check for name conflict + origname = projname + newproject = self.projectdir + '/' + projname + newname = projname + while os.path.isdir(newproject) or self.blacklisted(newname): + if self.blacklisted(newname): + warning = "Name " + newname + " is not allowed for a project name." + elif badrex1.match(newname): + warning = 'project name may not start with "."' + elif badrex2.match(newname): + warning = 'project name contains illegal characters or whitespace.' + else: + warning = "Project " + newname + " already exists!" + newname = ProjectNameDialog(self, warning, seed=newname).result + if not newname: + return 0 # Canceled, no action. + newproject = self.projectdir + '/' + newname + print("New project name is " + newname + ".") + # (3) Create new directory + os.makedirs(newproject) + # (4) Dump the tarball (if any) in the new directory + if tarname: + with tarfile.open(tarname, mode='r:gz') as archive: + for member in archive: + archive.extract(member, newproject) + # (5) Copy the JSON document into the new directory. Keep the + # original name of the project, so as to overwrite any existing + # document, then change the name to match that of the project + # folder. + # New behavior 12/2018: JSON file is always called 'project.json'. + # Also support legacy JSON name if it exists (don't generate files with + # both names) + + jsonfile = newproject + '/project.json' + if not os.path.isfile(jsonfile): + if os.path.isfile(newproject + '/' + projname + '.json'): + jsonfile = newproject + '/' + projname + '.json' + + try: + shutil.copy(importfile, jsonfile) + except IOError as e: + print('Error copying files: ' + str(e)) + return None + else: + # If filename is 'project.json' then it does not need to be changed. + # This is for legacy name support only. + if jsonfile != newproject + '/project.json': + shutil.move(jsonfile, newproject + '/' + newname + '.json') + + # (6) Remove the original files from the import folder + os.remove(importfile) + if tarname: + os.remove(tarname) + + # (7) Standard project setup: if spi/, elec/, and ngspice/ do not + # exist, create them. If elec/.java does not exist, create it and + # seed from deskel. If ngspice/run and ngspice/run/.allwaves do not + # exist, create them. + + if not os.path.exists(newproject + '/spi'): + os.makedirs(newproject + '/spi') + if not os.path.exists(newproject + '/spi/pex'): + os.makedirs(newproject + '/spi/pex') + if not os.path.exists(newproject + '/spi/lvs'): + os.makedirs(newproject + '/spi/lvs') + if not os.path.exists(newproject + '/ngspice'): + os.makedirs(newproject + '/ngspice') + if not os.path.exists(newproject + '/ngspice/run'): + os.makedirs(newproject + '/ngspice/run') + if not os.path.exists(newproject + '/ngspice/run/.allwaves'): + os.makedirs(newproject + '/ngspice/run/.allwaves') + if not os.path.exists(newproject + '/elec'): + os.makedirs(newproject + '/elec') + if not os.path.exists(newproject + '/xcirc'): + os.makedirs(newproject + '/xcirc') + if not os.path.exists(newproject + '/mag'): + os.makedirs(newproject + '/mag') + + self.reinitElec(newproject) # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any + + return 1 # Success + + #------------------------------------------------------------------------ + # Import for netlists (.spi): + # (1) Request project name + # (2) Create new project if name does not exist, or + # place netlist in existing project if it does. + #------------------------------------------------------------------------ + + #-------------------------------------------------------------------- + # Install netlist in electric: + # "importfile" is the filename in ~/design/import + # "pname" is the name of the target project (folder) + # "newfile" is the netlist file name (which may or may not be the same + # as 'importfile'). + #-------------------------------------------------------------------- + + def install_in_electric(self, importfile, pname, newfile, isnew=True): + #-------------------------------------------------------------------- + # Install the netlist. + # If netlist is CDL, then call cdl2spi first + #-------------------------------------------------------------------- + + newproject = self.projectdir + '/' + pname + if not os.path.isdir(newproject + '/spi/'): + os.makedirs(newproject + '/spi/') + if os.path.splitext(newfile)[1] == '.cdl': + if not os.path.isdir(newproject + '/cdl/'): + os.makedirs(newproject + '/cdl/') + shutil.copy(importfile, newproject + '/cdl/' + newfile) + try: + p = subprocess.run(['/ef/apps/bin/cdl2spi', importfile], + stdout = subprocess.PIPE, stderr = subprocess.PIPE, + check = True) + except subprocess.CalledProcessError as e: + print('Error running cdl2spi: ' + e.output.decode('utf-8')) + if isnew == True: + shutil.rmtree(newproject) + return None + else: + spi_string = p.stdout.splitlines()[0].decode('utf-8') + if p.stderr: + err_string = p.stderr.splitlines()[0].decode('utf-8') + # Print error messages to console + print(err_string) + if not spi_string: + print('Error: cdl2spi has no output') + if isnew == True: + shutil.rmtree(newproject) + return None + outname = os.path.splitext(newproject + '/spi/' + newfile)[0] + '.spi' + with open(outname, 'w') as f: + f.write(spi_string) + else: + outname = newproject + '/spi/' + newfile + try: + shutil.copy(importfile, outname) + except IOError as e: + print('Error copying files: ' + str(e)) + if isnew == True: + shutil.rmtree(newproject) + return None + + #-------------------------------------------------------------------- + # Symbol generator---this code to be moved into its own def. + #-------------------------------------------------------------------- + # To-do, need a more thorough SPICE parser, maybe use netgen to parse. + # Need to find topmost subcircuit, by parsing the hieararchy. + subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE) + subnames = [] + with open(importfile, 'r') as f: + for line in f: + lmatch = subcktrex.match(line) + if lmatch: + subnames.append(lmatch.group(1)) + + if subnames: + subname = subnames[0] + + # Run cdl2icon perl script + try: + p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname', + subname, '-libname', pname, '-projname', pname, '--prntgussddirs'], + stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True) + except subprocess.CalledProcessError as e: + print('Error running cdl2spi: ' + e.output.decode('utf-8')) + return None + else: + pin_string = p.stdout.splitlines()[0].decode('utf-8') + if not pin_string: + print('Error: cdl2icon has no output') + if isnew == True: + shutil.rmtree(newproject) + return None + if p.stderr: + err_string = p.stderr.splitlines()[0].decode('utf-8') + print(err_string) + + # Invoke dialog to arrange pins here + pin_info_list = SymbolBuilder(self, pin_string.split(), fontsize=self.prefs['fontsize']).result + if not pin_info_list: + # Dialog was canceled + print("Symbol builder was canceled.") + if isnew == True: + shutil.rmtree(newproject) + return 0 + + for pin in pin_info_list: + pin_info = pin.split(':') + pin_name = pin_info[0] + pin_type = pin_info[1] + + # Call cdl2icon with the final pin directions + outname = newproject + '/elec/' + pname + '.delib/' + os.path.splitext(newfile)[0] + '.ic' + try: + p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname', + subname, '-libname', pname, '-projname', pname, '-output', + outname, '-pindircmbndstring', ','.join(pin_info_list)], + stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True) + except subprocess.CalledProcessError as e: + print('Error running cdl2icon: ' + e.output.decode('utf-8')) + if isnew == True: + shutil.rmtree(newproject) + return None + else: + icon_string = p.stdout.splitlines()[0].decode('utf-8') # not used, AFAIK + if p.stderr: + err_string = p.stderr.splitlines()[0].decode('utf-8') + print(err_string) + + return 1 # Success + + #------------------------------------------------------------------------ + # Import netlist file into existing project + #------------------------------------------------------------------------ + + def importspiceinto(self, newfile, importfile): + # Require existing project location + ppath = ExistingProjectDialog(self, self.get_project_list()).result + if not ppath: + return 0 # Canceled in dialog, no action. + pname = os.path.split(ppath)[1] + print("Importing into existing project " + pname) + result = self.install_in_electric(importfile, pname, newfile, isnew=False) + if result == None: + print('Error during import.') + return None + elif result == 0: + return 0 # Canceled + else: + # Remove original file from imports area + os.remove(importfile) + return 1 # Success + + #------------------------------------------------------------------------ + # Import netlist file as a new project + #------------------------------------------------------------------------ + + def importspice(self, newfile, importfile): + # Use create project code first to generate a valid project space. + newname = self.createproject(None) + if not newname: + return 0 # Canceled in dialog, no action. + print("Importing as new project " + newname + ".") + result = self.install_in_electric(importfile, newname, newfile, isnew=True) + if result == None: + print('Error during install') + return None + elif result == 0: + # Canceled, so do not remove the import + return 0 + else: + # Remove original file from imports area + os.remove(importfile) + return 1 # Success + + #------------------------------------------------------------------------ + # Determine if JSON's tar can be imported as-if it were just a *.v. + # This is thin wrapper around tarVglImportable. Find the JSON's associated + # tar.gz if any, and call tarVglImportable. + # Returns list of two: + # None if rules not satisified; else path of the single GL .v member. + # None if rules not satisified; else root-name of the single .json member. + #------------------------------------------------------------------------ + + def jsonTarVglImportable(self, path): + ext = os.path.splitext(path)[1] + if ext != '.json': return None, None, None + + tar = self.json2targz(path) + if not tar: return None, None, None + + return self.tarVglImportable(tar) + + #------------------------------------------------------------------------ + # Get a single named member (memPath) out of a JSON's tar file. + # This is thin wrapper around tarMember2tempfile. Find the JSON's associated + # tar.gz if any, and call tarMember2tempfile. + #------------------------------------------------------------------------ + + def jsonTarMember2tempfile(self, path, memPath): + ext = os.path.splitext(path)[1] + if ext != '.json': return None + + tar = self.json2targz(path) + if not tar: return None + + return self.tarMember2tempfile(tar, memPath) + + #------------------------------------------------------------------------ + # Determine if tar-file can be imported as-if it were just a *.v. + # Require exactly one yosys-output .netlist.v, and exactly one .json. + # Nothing else matters: Ignore all other *.v, *.tv, *.jelib, *.vcd... + # + # If user renames *.netlist.v in cloudv before export to not end in + # netlist.v, we won't recognize it. + # + # Returns list of two: + # None if rules not satisified; else path of the single GL netlist.v member. + # None if rules not satisified; else root-name of the single .json member. + #------------------------------------------------------------------------ + + def tarVglImportable(self, path): + # count tar members by extensions. Track the .netlist.v. and .json. Screw the rest. + nbrExt = {'.v':0, '.netlist.v':0, '.tv':0, '.jelib':0, '.json':0, '/other/':0, '/vgl/':0} + nbrGLv = 0 + jname = None + vfile = None + node = None + t = tarfile.open(path) + for i in t: + # ignore (without counting) dir entries. From cloudv (so far) the tar does not + # have dir-entries, but most tar do (esp. most manually made test cases). + if i.isdir(): + continue + # TODO: should we require all below counted files to be plain files (no symlinks etc.)? + # get extension, but recognize a multi-ext for .netlist.v case + basenm = os.path.basename(i.name) + ext = os.path.splitext(basenm)[1] + root = os.path.splitext(basenm)[0] + ext2 = os.path.splitext(root)[1] + if ext2 == '.netlist' and ext == '.v': + ext = ext2 + ext + if ext and ext not in nbrExt: + ext = '/other/' + elif ext == '.netlist.v' and self.tarMemberIsGLverilog(t, i.name): + vfile = i.name + ext = '/vgl/' + elif ext == '.json': + node = self.tarMemberHasFoundryNode(t, i.name) + jname = root + nbrExt[ext] += 1 + + # check rules. Require exactly one yosys-output .netlist.v, and exactly one .json. + # Quantities of other types are all don't cares. + if (nbrExt['/vgl/'] == 1 and nbrExt['.json'] == 1): + # vfile is the name of the verilog netlist in the tarball, while jname + # is the root name of the JSON file found in the tarball (if any) + return vfile, jname, node + + # failed, not gate-level-verilog importable: + return None, None, node + + + #------------------------------------------------------------------------ + # OBSOLETE VERSION: Determine if tar-file can be imported as-if it were just a *.v. + # Rules for members: one *.v, {0,1} *.jelib, {0,1} *.json, 0 other types. + # Return None if rules not satisified; else return path of the single .v. + #------------------------------------------------------------------------ + # + # def tarVglImportable(self, path): + # # count tar members by extensions. Track the .v. + # nbrExt = {'.v':0, '.jelib':0, '.json':0, 'other':0} + # vfile = "" + # t = tarfile.open(path) + # for i in t: + # ext = os.path.splitext(i.name)[1] + # if ext not in nbrExt: + # ext = 'other' + # nbrExt[ext] += 1 + # if ext == ".v": vfile = i.name + # + # # check rules. + # if (nbrExt['.v'] != 1 or nbrExt['other'] != 0 or + # nbrExt['.jelib'] > 1 or nbrExt['.json'] > 1): + # return None + # return vfile + + #------------------------------------------------------------------------ + # Get a single named member (memPath) out of a tar file (tarPath), into a + # temp-file, so subprocesses can process it. + # Return path to the temp-file, or None if member not found in the tar. + #------------------------------------------------------------------------ + + def tarMember2tempfile(self, tarPath, memPath): + t = tarfile.open(tarPath) + member = t.getmember(memPath) + if not member: return None + + # Change member.name so it extracts into our new temp-file. + # extract() can specify the root-dir befow which the member path + # resides. If temp is an absolute-path, that root-dir must be /. + tmpf1 = tempfile.NamedTemporaryFile(delete=False) + if tmpf1.name[0] != "/": + raise ValueError("assertion failed, temp-file path not absolute: %s" % tmpf1.name) + member.name = tmpf1.name + t.extract(member,"/") + + return tmpf1.name + + #------------------------------------------------------------------------ + # Create an electric .delib directory and seed it with a header file + #------------------------------------------------------------------------ + + def create_electric_header_file(self, project, libname): + if not os.path.isdir(project + '/elec/' + libname + '.delib'): + os.makedirs(project + '/elec/' + libname + '.delib') + + p = subprocess.run(['electric', '-v'], stdout=subprocess.PIPE) + eversion = p.stdout.splitlines()[0].decode('utf-8') + # Create header file + with open(project + '/elec/' + libname + '.delib/header', 'w') as f: + f.write('# header information:\n') + f.write('H' + libname + '|' + eversion + '\n\n') + f.write('# Tools:\n') + f.write('Ouser|DefaultTechnology()Sschematic\n') + f.write('Osimulation|VerilogUseAssign()BT\n') + f.write('C____SEARCH_FOR_CELL_FILES____\n') + + #------------------------------------------------------------------------ + # Create an ad-hoc "project.json" dictionary and fill essential records + #------------------------------------------------------------------------ + + def create_ad_hoc_json(self, ipname, pname): + # Create ad-hoc JSON file and fill it with the minimum + # necessary entries to define a project. + jData = {} + jDS = {} + jDS['ip-name'] = ipname + pdkdir = self.get_pdk_dir(pname, path=True) + try: + jDS['foundry'], jDS['node'], pdk_desc, pdk_stat = self.pdkdir2fnd( pdkdir ) + except: + # Cannot parse PDK name, so foundry and node will remain undefined + pass + jDS['format'] = '3' + pparams = [] + param = {} + param['unit'] = "\u00b5m\u00b2" + param['condition'] = "device_area" + param['display'] = "Device area" + pmax = {} + pmax['penalty'] = '0' + pmax['target'] = '100000' + param['max'] = pmax + pparams.append(param) + + param = {} + param['unit'] = "\u00b5m\u00b2" + param['condition'] = "area" + param['display'] = "Layout area" + pmax = {} + pmax['penalty'] = '0' + pmax['target'] = '100000' + param['max'] = pmax + pparams.append(param) + + param = {} + param['unit'] = "\u00b5m" + param['condition'] = "width" + param['display'] = "Layout width" + pmax = {} + pmax['penalty'] = '0' + pmax['target'] = '300' + param['max'] = pmax + pparams.append(param) + + param = {} + param['condition'] = "DRC_errors" + param['display'] = "DRC errors" + pmax = {} + pmax['penalty'] = 'fail' + pmax['target'] = '0' + param['max'] = pmax + pparams.append(param) + + param = {} + param['condition'] = "LVS_errors" + param['display'] = "LVS errors" + pmax = {} + pmax['penalty'] = 'fail' + pmax['target'] = '0' + param['max'] = pmax + pparams.append(param) + + jDS['physical-params'] = pparams + jData['data-sheet'] = jDS + + return jData + + #------------------------------------------------------------------------ + # For a single named member (memPath) out of an open tarfile (tarf), + # determine if it is a JSON file, and attempt to extract value of entry + # 'node' in dictionary entry 'data-sheet'. Otherwise return None. + #------------------------------------------------------------------------ + + def tarMemberHasFoundryNode(self, tarf, memPath): + fileJSON = tarf.extractfile(memPath) + if not fileJSON: return None + + try: + # NOTE: tarfile data is in bytes, json.load(fileJSON) does not work. + datatop = json.loads(fileJSON.read().decode('utf-8')) + except: + print("Failed to load extract file " + memPath + " as JSON data") + return None + else: + node = None + if 'data-sheet' in datatop: + dsheet = datatop['data-sheet'] + if 'node' in dsheet: + node = dsheet['node'] + + fileJSON.close() # close open-tarfile before any return + return node + + #------------------------------------------------------------------------ + # For a single named member (memPath) out of an open tarfile (tarf), + # determine if first line embeds (case-insensitive match): Generated by Yosys + # Return True or False. If no such member or it has no 1st line, returns False. + #------------------------------------------------------------------------ + + def tarMemberIsGLverilog(self, tarf, memPath): + fileHdl = tarf.extractfile(memPath) + if not fileHdl: return False + + line = fileHdl.readline() + fileHdl.close() # close open-tarfile before any return + if not line: return False + return ('generated by yosys' in line.decode('utf-8').lower()) + + #------------------------------------------------------------------------ + # Import vgl-netlist file INTO existing project. + # The importfile can be a .v; or a .json-with-tar that embeds a .v. + # What is newfile? not used here. + # + # PROMPT to select an existing project is here. + # (Is also a PROMPT to select existing electric lib, but that's within importvgl). + #------------------------------------------------------------------------ + + def importvglinto(self, newfile, importfile): + # Require existing project location + ppath = ExistingProjectDialog(self, self.get_project_list()).result + if not ppath: return 0 # Canceled in dialog, no action. + pname = os.path.split(ppath)[1] + print( "Importing into existing project: %s" % (pname)) + + return self.importvgl(newfile, importfile, pname) + + #------------------------------------------------------------------------ + # Import cloudv project as new project. + #------------------------------------------------------------------------ + + def install_from_cloudv(self, opath, ppath, pdkname, stdcellname, ydicts): + oname = os.path.split(opath)[1] + pname = os.path.split(ppath)[1] + + print('Cloudv project name is ' + str(oname)) + print('New Open Galaxy project name is ' + str(pname)) + + os.makedirs(ppath + '/verilog', exist_ok=True) + + vfile = None + isfullchip = False + ipname = oname + + # First check for single synthesized projects, or all synthesized + # digital sub-blocks within a full-chip project. + + os.makedirs(ppath + '/verilog/source', exist_ok=True) + bfiles = glob.glob(opath + '/build/*.netlist.v') + for bfile in bfiles: + tname = os.path.split(bfile)[1] + vname = os.path.splitext(os.path.splitext(tname)[0])[0] + tfile = ppath + '/verilog/' + vname + '/' + vname + '.vgl' + print('Making qflow sub-project ' + vname) + os.makedirs(ppath + '/verilog/' + vname, exist_ok=True) + shutil.copy(bfile, tfile) + if vname == oname: + vfile = tfile + + # Each build project gets its own qflow directory. Create the + # source/ subdirectory and make a link back to the .vgl file. + # qflow prep should do the rest. + + os.makedirs(ppath + '/qflow', exist_ok=True) + os.makedirs(ppath + '/qflow/' + vname) + os.makedirs(ppath + '/qflow/' + vname + '/source') + + # Make sure the symbolic link is relative, so that it is portable + # through a shared project. + curdir = os.getcwd() + os.chdir(ppath + '/qflow/' + vname + '/source') + os.symlink('../../../verilog/' + vname + '/' + vname + '.vgl', vname + '.v') + os.chdir(curdir) + + # Create a simple qflow_vars.sh file so that the project manager + # qflow launcher will see it as a qflow sub-project. If the meta.yaml + # file has a "stdcell" entry for the subproject, then add the line + # "techname=" with the name of the standard cell library as pulled + # from meta.yaml. + + stdcell = None + buildname = 'build/' + vname + '.netlist.v' + for ydict in ydicts: + if buildname in ydict: + yentry = ydict[buildname] + if 'stdcell' in yentry: + stdcell = yentry['stdcell'] + + with open(ppath + '/qflow/' + vname + '/qflow_vars.sh', 'w') as ofile: + print('#!/bin/tcsh -f', file=ofile) + if stdcell: + print('set techname=' + stdcell, file=ofile) + + # Now check for a full-chip verilog SoC (from CloudV) + + modrex = re.compile('[ \t]*module[ \t]+[^ \t(]*_?soc[ \t]*\(') + genmodrex = re.compile('[ \t]*module[ \t]+([^ \t(]+)[ \t]*\(') + + bfiles = glob.glob(opath + '/*.model/*.v') + for bfile in bfiles: + tname = os.path.split(bfile)[1] + vpath = os.path.split(bfile)[0] + ipname = os.path.splitext(tname)[0] + tfile = ppath + '/verilog/' + ipname + '.v' + isfullchip = True + break + + if isfullchip: + print('Cloudv project IP name is ' + str(ipname)) + + # All files in */ paths should be copied to project verilog/source/, + # except for the module containing the SoC itself. Note that the actual + # verilog source goes here, not the synthesized netlist, although that is + # mainly for efficiency of the simulation, which would normally be done in + # cloudV and not in Open Galaxy. For Open Galaxy, what is needed is the + # existence of a verilog file containing a module name, which is used to + # track down the various files (LEF, DEF, etc.) that are needed for full- + # chip layout. + # + # (Sept. 2019) Added copying of files in /SW/ -> /sw/ and /Verify/ -> + # /verify/ for running full-chip simulations on the Open Galaxy side. + + os.makedirs(ppath + '/verilog', exist_ok=True) + + cfiles = glob.glob(vpath + '/source/*') + for cfile in cfiles: + cname = os.path.split(cfile)[1] + if cname != tname: + tpath = ppath + '/verilog/source/' + cname + os.makedirs(ppath + '/verilog/source', exist_ok=True) + shutil.copy(cfile, tpath) + + cfiles = glob.glob(vpath + '/verify/*') + for cfile in cfiles: + cname = os.path.split(cfile)[1] + tpath = ppath + '/verilog/verify/' + cname + os.makedirs(ppath + '/verilog/verify', exist_ok=True) + shutil.copy(cfile, tpath) + + cfiles = glob.glob(vpath + '/sw/*') + for cfile in cfiles: + cname = os.path.split(cfile)[1] + tpath = ppath + '/verilog/sw/' + cname + os.makedirs(ppath + '/verilog/sw', exist_ok=True) + shutil.copy(cfile, tpath) + + # Read the top-level SoC verilog and recast it for OpenGalaxy. + with open(bfile, 'r') as ifile: + chiplines = ifile.read().splitlines() + + # Find the modules used, track them down, and add the source location + # in the Open Galaxy environment as an "include" line in the top level + # verilog. + + parentdir = os.path.split(bfile)[0] + modfile = parentdir + '/docs/modules.txt' + + modules = [] + if os.path.isfile(modfile): + with open(modfile, 'r') as ifile: + modules = ifile.read().splitlines() + else: + print("Warning: No modules.txt file for the chip top level module in " + + parentdir + "/docs/.\n") + + # Get the names of verilog libraries in this PDK. + pdkdir = os.path.realpath(ppath + '/.ef-config/techdir') + pdkvlog = pdkdir + '/libs.ref/verilog' + pdkvlogfiles = glob.glob(pdkvlog + '/*/*.v') + + # Read the verilog libraries and create a dictionary mapping each + # module name to a location of the verilog file where it is located. + moddict = {} + for vlogfile in pdkvlogfiles: + with open(vlogfile, 'r') as ifile: + for line in ifile.read().splitlines(): + mmatch = genmodrex.match(line) + if mmatch: + modname = mmatch.group(1) + moddict[modname] = vlogfile + + # Get the names of verilog libraries in the user IP space. + # (TO DO: Need to know the IP version being used!) + designdir = os.path.split(ppath)[0] + ipdir = designdir + '/ip/' + uservlogfiles = glob.glob(ipdir + '/*/*/verilog/*.v') + for vlogfile in uservlogfiles: + # Strip ipdir from the front + vlogpath = vlogfile.replace(ipdir, '', 1) + with open(vlogfile, 'r') as ifile: + for line in ifile.read().splitlines(): + mmatch = genmodrex.match(line) + if mmatch: + modname = mmatch.group(1) + moddict[modname] = vlogpath + + # Find all netlist builds from the project (those that were copied above) + buildfiles = glob.glob(ppath + '/verilog/source/*.v') + for vlogfile in buildfiles: + # Strip ipdir from the front + vlogpath = vlogfile.replace(ppath + '/verilog/source/', '', 1) + with open(vlogfile, 'r') as ifile: + for line in ifile.read().splitlines(): + mmatch = genmodrex.match(line) + if mmatch: + modname = mmatch.group(1) + moddict[modname] = vlogpath + + # (NOTE: removing 'ifndef LVS' as netgen should be able to handle + # the contents of included files, and they are preferred since any + # arrays are declared in each module I/O) + # chiplines.insert(0, '`endif') + chiplines.insert(0, '//--- End of list of included module dependencies ---') + includedfiles = [] + for module in modules: + # Determine where this module comes from. Look in the PDK, then in + # the user ip/ directory, then in the local hierarchy. Note that + # the local hierarchy expects layouts from synthesized netlists that + # have not yet been created, so determine the expected location. + + if module in moddict: + if moddict[module] not in includedfiles: + chiplines.insert(0, '`include "' + moddict[module] + '"') + includedfiles.append(moddict[module]) + + # chiplines.insert(0, '`ifndef LVS') + chiplines.insert(0, '//--- List of included module dependencies ---') + chiplines.insert(0, '// iverilog simulation requires the use of -I source -I ~/design/ip') + chiplines.insert(0, '// NOTE: Includes may be rooted at ~/design/ip/ or at ./source') + chiplines.insert(0, '// SoC top level verilog copied and modified by project manager') + + # Copy file, but replace the module name "soc" with the ip-name + with open(tfile, 'w') as ofile: + for chipline in chiplines: + print(modrex.sub('module ' + ipname + ' (', chipline), file=ofile) + + # Need to define behavior: What if there is more than one netlist? + # Which one is to be imported? For now, ad-hoc behavior is to select + # the last netlist file in the list if no file matches the ip-name. + + # Note that for full-chip projects, the full chip verilog file is always + # the last one set. + + if not vfile: + try: + vfile = tfile + except: + pass + + # NOTE: vfile was being used to create a symbol, but not any more; + # see below. All the above code referencing vfile can probably be + # removed. + + try: + sfiles = glob.glob(vpath + '/source/*') + sfiles.extend(glob.glob(vpath + '/*/source/*')) + except: + sfiles = glob.glob(opath + '/*.v') + sfiles.extend(glob.glob(opath + '/*.sv')) + sfiles.extend(glob.glob(opath + '/local/*')) + + for fname in sfiles: + sname = os.path.split(fname)[1] + tfile = ppath + '/verilog/source/' + sname + # Reject '.model' and '.soc" files (these are meaningful only to CloudV) + fileext = os.path.splitext(fname)[1] + if fileext == '.model' or fileext == '.soc': + continue + if os.path.isfile(fname): + # Check if /verilog/source/ has been created + if not os.path.isdir(ppath + '/verilog/source'): + os.makedirs(ppath + '/verilog/source') + shutil.copy(fname, tfile) + + # Add standard cell library name to project.json + pjsonfile = ppath + '/project.json' + if os.path.exists(pjsonfile): + with open(pjsonfile, 'r') as ifile: + datatop = json.load(ifile) + else: + datatop = self.create_ad_hoc_json(ipname, ppath) + + # Generate a symbol in electric for the verilog top module + iconfile = ppath + '/elec/' + ipname + '.delib/' + ipname + '.ic' + if not os.path.exists(iconfile): + # NOTE: Symbols are created by qflow migration for project + # builds. Only the chip top-level needs to run create_symbol + # here. + + if isfullchip: + print("Creating symbol for module " + ipname + " automatically from verilog source.") + create_symbol(ppath, vfile, ipname, iconfile, False) + # Add header file + self.create_electric_header_file(ppath, ipname) + + dsheet = datatop['data-sheet'] + if not stdcellname or stdcellname == "": + dsheet['standard-cell'] = 'default' + else: + dsheet['standard-cell'] = stdcellname + + with open(pjsonfile, 'w') as ofile: + json.dump(datatop, ofile, indent = 4) + + return 0 + + #------------------------------------------------------------------------ + # Import vgl-netlist AS new project. + # The importfile can be a .v; or a .json-with-tar that embeds a .v. + # What is newfile? not used here. + # + # PROMPT to select an create new project is within importvgl. + #------------------------------------------------------------------------ + + def importvglas(self, newfile, importfile, seedname): + print('importvglas: seedname is ' + str(seedname)) + return self.importvgl(newfile, importfile, newname=None, seedname=seedname) + + #------------------------------------------------------------------------ + # Utility shared/used by both: Import vgl-netlist file AS or INTO a project. + # Called directly for AS. Called via importvglinto for INTO. + # importfile : source of .v to import, actual .v or json-with-tar that embeds a .v + # newfile : not used + # newname : target project-name (INTO), or None (AS: i.e. prompt to create one). + # Either newname is given: we PROMPT to pick an existing elecLib; + # Else PROMPT for new projectName and CREATE it (and use elecLib of same name). + #------------------------------------------------------------------------ + + def importvgl(self, newfile, importfile, newname=None, seedname=None): + elecLib = None + isnew = not newname + + # Up front: Determine if this import has a .json file associated + # with it. If so, then parse the JSON data to find if there is a + # foundry and node set for the project. If so, then the foundry + # node is not selectable at time of import. Likewise, if "isnew" + # is false, then we need to check if there is a directory called + # "newname" and if it is set to the same foundry node. If not, + # then the import must be rejected. + + tarVfile, jName, importnode = self.jsonTarVglImportable(importfile) + + if isnew: + print('importvgl: seedname is ' + str(seedname)) + # Use create project code first to generate a valid project space. + newname = self.createproject(None, seedname, importnode) + if not newname: return 0 # Canceled in dialog, no action. + print("Importing as new project " + newname + ".") + elecLib = newname + + ppath = self.projectdir + '/' + newname + if not elecLib: + choices = self.get_elecLib_list(newname) + if not choices: + print( "Aborted: No existing electric libraries found to import into.") + return 0 + + elecLib = ExistingElecLibDialog(self, choices).result + if not elecLib: + # Never a just-created project to delete here: We only PROMPT to pick elecLib in non-new case. + return 0 # Canceled in dialog, no action. + + # Isolate just electric lib name without extension. ../a/b.delib -> b + elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0] + print("Importing to project: %s, elecLib: %s" % (newname, elecLib)) + + # Determine isolated *.v as importactual. May be importfile or tar-member (as temp-file). + importactual = importfile + if tarVfile: + importactual = self.jsonTarMember2tempfile(importfile, tarVfile) + print("importing json-with-tar's member: %s" % (tarVfile)) + + if not os.path.isfile(importactual): + # TODO: should this be a raise instead? + print('Error determining *.v to import') + return None + + result = self.vgl_install(importactual, newname, elecLib, newfile, isnew=isnew) + if result == None: + print('Error during install') + return None + elif result == 0: + # Canceled, so do not remove the import + return 0 + else: + # If jName is non-NULL then there is a JSON file in the tarball. This is + # to be used as the project JSON file. Contents of file coming from + # CloudV are correct as of 12/8/2017. + pname = os.path.expanduser('~/design/' + newname) + legacyjname = pname + '/' + newname + '.json' + # New behavior 12/2018: Project JSON file always named 'project.json' + jname = pname + '/project.json' + + # Do not overwrite an existing JSON file. Overwriting is a problem for + # "import into", as the files go into an existing project, which would + # normally have its own JSON file. + + if not os.path.exists(jname) and not os.path.exists(legacyjname): + try: + tarJfile = os.path.split(tarVfile)[0] + '/' + jName + '.json' + importjson = self.jsonTarMember2tempfile(importfile, tarJfile) + except: + jData = self.create_ad_hoc_json(newname, pname) + + with open(jname, 'w') as ofile: + json.dump(jData, ofile, indent = 4) + + else: + # Copy the temporary file pulled from the tarball and + # remove the temporary file. + shutil.copy(importjson, jname) + os.remove(importjson) + + # For time-being, if a tar.gz & json: archive them in the target project, also as extracted. + # Remove original file from imports area (either .v; or .json plus tar) + # plus temp-file if extracted from the tar. + if importactual != importfile: + os.remove(importactual) + pname = self.projectdir + '/' + newname + importd = pname + '/' + archiveimportdir # global: archiveimportdir + os.makedirs(importd, exist_ok=True) + # Dirnames to embed a VISIBLE date (UTC) of when populated. + # TODO: improve dir naming or better way to store & understand later when it was processed (a log?), + # without relying on file-system mtime. + archived = tempfile.mkdtemp( dir=importd, prefix='{:%Y-%m-%d.%H:%M:%S}-'.format(datetime.datetime.utcnow())) + tarname = self.json2targz(importfile) + if tarname: + with tarfile.open(tarname, mode='r:gz') as archive: + for member in archive: + archive.extract(member, archived) + self.moveJsonPlus(importfile, archived) + else: + self.removeJsonPlus(importfile) + return 1 # Success + + #------------------------------------------------------------------------ + # Prepare multiline "warning" indicating which files to install already exist. + # TODO: ugly, don't use a simple confirmation dialogue: present a proper table. + #------------------------------------------------------------------------ + def installsConfirmMarkOverwrite(self, module, files): + warning = [ "For import of module: %s," % module ] + anyExists = False + for i in files: + exists = os.path.isfile(os.path.expanduser(i)) + if exists: anyExists = True + warning += [ (" * " if exists else " ") + i ] + if anyExists: + titleSuffix = "\nCONFIRM installation of (*: OVERWRITE existing):" + else: + titleSuffix = "\nCONFIRM installation of:" + warning[0] += titleSuffix + return ConfirmInstallDialog(self, "\n".join(warning)).result + + def vgl_install(self, importfile, pname, elecLib, newfile, isnew=True): + #-------------------------------------------------------------------- + # Convert the in .v to: spi, cdl, elec-icon, elec-text-view forms. + # TODO: Prompt to confirm final install of 5 files in dir-structure. + # + # newfile: argument is not used. What is it for? + # Target project AND electricLib MAY BE same (pname) or different. + # Rest of the filenames are determined by the module name in the source .v. + #-------------------------------------------------------------------- + + newproject = self.projectdir + '/' + pname + try: + p = subprocess.run(['/ef/apps/bin/vglImport', importfile, pname, elecLib], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True, universal_newlines=True) + except subprocess.CalledProcessError as e: + if hasattr(e, 'stdout') and e.stdout: print(e.stdout) + if hasattr(e, 'stderr') and e.stderr: print(e.stderr) + print('Error running vglImport: ' + str(e)) + if isnew == True: shutil.rmtree(newproject) + return None + else: + dataLines = p.stdout.splitlines() + if p.stderr: + # Print error messages to console + for i in p.stderr.splitlines(): print(i) + if not dataLines or len(dataLines) != 11: + print('Error: vglImport has no output, or wrong #outputs (%d vs 11)' % len(dataLines)) + if isnew == True: shutil.rmtree(newproject) + return None + else: + module = dataLines[0] + confirm = self.installsConfirmMarkOverwrite(module, dataLines[2::2]) + if not confirm: + print("Cancelled") + if isnew == True: shutil.rmtree(newproject) + return 0 + # print("Proceed") + clean = dataLines[1:] + nbr = len(dataLines) + ndx = 1 + # trap I/O errors and clean-up if any + try: + while ndx+1 < nbr: + trg = os.path.expanduser(dataLines[ndx+1]) + os.makedirs(os.path.dirname(trg), exist_ok=True) + shutil.move(dataLines[ndx], trg) + ndx += 2 + except IOError as e: + print('Error copying files: ' + str(e)) + for i in clean: + with contextlib.suppress(FileNotFoundError): os.remove(i) + if isnew == True: shutil.rmtree(newproject) + return 0 + print( "For import of module %s installed: %s" % (module, " ".join(dataLines[2::2]))) + return 1 # Success + + + #------------------------------------------------------------------------ + # Callback function from "Import Into" button on imports list box. + #------------------------------------------------------------------------ + + def importintodesign(self, value): + if not value['values']: + print('No import selected.') + return + + # Stop the watchdog timer while this is going on + self.watchclock.stop() + newname = value['text'] + importfile = value['values'][0] + print('Import project name: ' + newname + '') + print('Import file name: ' + importfile + '') + + # Behavior depends on what kind of file is being imported. + # Tarballs are entire projects. Other files are individual + # files and may be imported into new or existing projects + + if os.path.isdir(importfile): + print('File is a project, must import as new project.') + result = self.import2project(importfile, addWarn='Redirected: A projectDir must Import-As new project.') + else: + ext = os.path.splitext(importfile)[1] + vFile, jName, importnode = self.jsonTarVglImportable(importfile) + if ((ext == '.json' and vFile) or ext == '.v'): + result = self.importvglinto(newname, importfile) + elif ext == '.json': + # Same behavior as "Import As", at least for now + print('File is a project, must import as new project.') + result = self.importjson(newname, importfile) + else: + result = self.importspiceinto(newname, importfile) + + if result: + self.update_project_views(force=True) + self.watchclock.restart() + + #------------------------------------------------------------------------ + # Callback function from "Import As" button on imports list box. + #------------------------------------------------------------------------ + + def importdesign(self, value): + if not value['values']: + print('No import selected.') + return + + # Stop the watchdog timer while this is going on + self.watchclock.stop() + newname = value['text'] + importfile = value['values'][0] + print('Import project name: ' + newname) + print('Import file name: ' + importfile) + + # Behavior depends on what kind of file is being imported. + # Tarballs are entire projects. Other files are individual + # files and may be imported into new or existing projects + + if os.path.isdir(importfile): + result = self.import2project(importfile) + else: + pathext = os.path.splitext(importfile) + vfile, seedname, importnode = self.jsonTarVglImportable(importfile) + if ((pathext[1] == '.json' and seedname) or pathext[1] == '.v'): + result = self.importvglas(newname, importfile, seedname) + elif pathext[1] == '.json': + result = self.importjson(newname, importfile) + else: + result = self.importspice(newname, importfile) + + if result: + self.update_project_views(force=True) + self.watchclock.restart() + + def deleteimport(self, value): + if not value['values']: + print('No import selected.') + return + + print("Delete import " + value['text'] + ' ' + value['values'][0] + " !") + # Require confirmation + warning = 'Confirm delete import ' + value['text'] + '?' + confirm = ProtectedConfirmDialog(self, warning).result + if not confirm == 'okay': + return + print('Delete confirmed!') + item = value['values'][0] + + if not os.path.islink(item) and os.path.isdir(item): + shutil.rmtree(item) + return + + os.remove(item) + ext = os.path.splitext(item) + # Where import is a pair of .json and .tar.gz files, remove both. + if ext[1] == '.json': + if os.path.exists(ext[0] + '.tar.gz'): + os.remove(ext[0] + '.tar.gz') + elif os.path.exists(ext[0] + '.tgz'): + os.remove(ext[0] + '.tgz') + + def update_project_views(self, force=False): + # More than updating project views, this updates projects, imports, and + # IP libraries. + + projectlist = self.get_project_list() + self.projectselect.repopulate(projectlist) + pdklist = self.get_pdk_list(projectlist) + self.projectselect.populate2("PDK", projectlist, pdklist) + + old_imports = self.number_of_imports + importlist = self.get_import_list() + self.importselect.repopulate(importlist) + valuelist = self.importselect.getvaluelist() + datelist = self.get_date_list(valuelist) + itemlist = self.importselect.getlist() + self.importselect.populate2("date", itemlist, datelist) + + # To do: Check if itemlist in imports changed, and open if a new import + # has arrived. + + if force or (old_imports != None) and (old_imports < self.number_of_imports): + self.import_open() + + iplist = self.get_library_list() + self.ipselect.repopulate(iplist, versioning=True) + valuelist = self.ipselect.getvaluelist() + datelist = self.get_date_list(valuelist) + itemlist = self.ipselect.getlist() + self.ipselect.populate2("date", itemlist, datelist) + + def update_alert(self): + # Project manager has been updated. Generate an alert window and + # provide option to restart the project manager. + + warning = 'Project manager app has been updated. Restart now?' + confirm = ConfirmDialog(self, warning).result + if not confirm == 'okay': + print('Warning: Must quit and restart to get any fixes or updates.') + return + os.execl('/ef/efabless/opengalaxy/og_gui_manager.py', 'appsel_zenity.sh') + # Does not return; replaces existing process. + + #---------------------------------------------------------------------- + # Delete a project from the design folder. + #---------------------------------------------------------------------- + + def deleteproject(self, value): + if not value['values']: + print('No project selected.') + return + print('Delete project ' + value['values'][0]) + # Require confirmation + warning = 'Confirm delete entire project ' + value['text'] + '?' + confirm = ProtectedConfirmDialog(self, warning).result + if not confirm == 'okay': + return + shutil.rmtree(value['values'][0]) + + #---------------------------------------------------------------------- + # Clean out the simulation folder. Traditionally this was named + # 'ngspice', so this is checked for backward-compatibility. The + # proper name of the simulation directory is 'simulation'. + #---------------------------------------------------------------------- + + def cleanproject(self, value): + if not value['values']: + print('No project selected.') + return + ppath = value['values'][0] + print('Clean simulation raw data from directory ' + ppath) + # Require confirmation + warning = 'Confirm clean project ' + value['text'] + ' contents?' + confirm = ConfirmDialog(self, warning).result + if not confirm == 'okay': + return + if os.path.isdir(ppath + '/simulation'): + simpath = 'simulation' + elif os.path.isdir(ppath + '/ngspice'): + simpath = 'ngspice' + else: + print('Project has no simulation folder.') + return + + filelist = os.listdir(ppath + '/' + simpath) + for sfile in filelist: + if os.path.splitext(sfile)[1] == '.raw': + os.remove(ppath + '/ngspice/' + sfile) + print('Project simulation folder cleaned.') + + # Also clean the log file + filelist = os.listdir(ppath) + for sfile in filelist: + if os.path.splitext(sfile)[1] == '.log': + os.remove(ppath + '/' + sfile) + + #--------------------------------------------------------------------------------------- + # Determine which schematic editors are compatible with the PDK, and return a list of them. + #--------------------------------------------------------------------------------------- + + def list_valid_schematic_editors(self, pdktechdir): + # Check PDK technology directory for xcircuit, xschem, and electric + applist = [] + if os.path.exists(pdktechdir + '/elec'): + applist.append('electric') + if os.path.exists(pdktechdir + '/xschem'): + applist.append('xschem') + if os.path.exists(pdktechdir + '/xcircuit'): + applist.append('xcircuit') + + return applist + + #------------------------------------------------------------------------------------------ + # Determine which layout editors are compatible with the PDK, and return a list of them. + #------------------------------------------------------------------------------------------ + + def list_valid_layout_editors(self, pdktechdir): + # Check PDK technology directory for magic and klayout + applist = [] + if os.path.exists(pdktechdir + '/magic'): + applist.append('magic') + if os.path.exists(pdktechdir + '/klayout'): + applist.append('klayout') + return applist + + #---------------------------------------------------------------------- + # Create a new project folder and initialize it (see below for steps) + #---------------------------------------------------------------------- + + def createproject(self, value, seedname=None, importnode=None): + # Note: value is current selection, if any, and is ignored + # Require new project location and confirmation + badrex1 = re.compile("^\.") + badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*") + warning = 'Create new project:' + print(warning) + development = self.prefs['development'] + while True: + try: + if seedname: + newname, newpdk = NewProjectDialog(self, warning, seed=seedname, importnode=importnode, development=development).result + else: + newname, newpdk = NewProjectDialog(self, warning, seed='', importnode=importnode, development=development).result + except TypeError: + # TypeError occurs when "Cancel" is pressed, just handle exception. + return None + if not newname: + return None # Canceled, no action. + + newproject = self.projectdir + '/' + newname + if self.blacklisted(newname): + warning = newname + ' is not allowed for a project name.' + elif badrex1.match(newname): + warning = 'project name may not start with "."' + elif badrex2.match(newname): + warning = 'project name contains illegal characters or whitespace.' + elif os.path.exists(newproject): + warning = newname + ' is already a project name.' + else: + break + + try: + + subprocess.Popen([config.apps_path + '/create_project.py', newproject, newpdk]) + + except IOError as e: + print('Error copying files: ' + str(e)) + return None + + except: + print('Error making project.') + return None + + return newname + ''' + # Find what tools are compatible with the given PDK + schemapps = self.list_valid_schematic_editors(newpdk + '/libs.tech') + layoutapps = self.list_valid_layout_editors(newpdk + '/libs.tech') + + print('New project name will be ' + newname + '.') + print('Associated project PDK is ' + newpdk + '.') + try: + os.makedirs(newproject) + + # Make standard folders + if 'magic' in layoutapps: + os.makedirs(newproject + '/mag') + + os.makedirs(newproject + '/spi') + os.makedirs(newproject + '/spi/pex') + os.makedirs(newproject + '/spi/lvs') + if 'electric' in layoutapps or 'electric' in schemapps: + os.makedirs(newproject + '/elec') + if 'xcircuit' in schemapps: + os.makedirs(newproject + '/xcirc') + if 'klayout' in schemapps: + os.makedirs(newproject + '/klayout') + os.makedirs(newproject + '/ngspice') + os.makedirs(newproject + '/ngspice/run') + if 'electric' in schemapps: + os.makedirs(newproject + '/ngspice/run/.allwaves') + os.makedirs(newproject + '/testbench') + os.makedirs(newproject + '/verilog') + os.makedirs(newproject + '/verilog/source') + os.makedirs(newproject + '/.ef-config') + if 'xschem' in schemapps: + os.makedirs(newproject + '/xschem') + + pdkname = os.path.split(newpdk)[1] + + # Symbolic links + os.symlink(newpdk, newproject + '/.ef-config/techdir') + + # Copy preferences + # deskel = '/ef/efabless/deskel' + # + # Copy examples (disabled; this is too confusing to the end user. Also, they + # should not be in user space at all, as they are not user editable. + # + # for item in os.listdir(deskel + '/exlibs'): + # shutil.copytree(deskel + '/exlibs/' + item, newproject + '/elec/' + item) + # for item in os.listdir(deskel + '/exmag'): + # if os.path.splitext(item)[1] == '.mag': + # shutil.copy(deskel + '/exmag/' + item, newproject + '/mag/' + item) + + # Put tool-specific startup files into the appropriate user directories. + if 'electric' in layoutapps or 'electric' in schemapps: + self.reinitElec(newproject) # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any + # Set up electric + self.create_electric_header_file(newproject, newname) + + if 'magic' in layoutapps: + shutil.copy(newpdk + '/libs.tech/magic/current/' + pdkname + '.magicrc', newproject + '/mag/.magicrc') + + if 'xcircuit' in schemapps: + xcircrc = newpdk + '/libs.tech/xcircuit/' + pdkname + '.' + 'xcircuitrc' + xcircrc2 = newpdk + '/libs.tech/xcircuit/xcircuitrc' + if os.path.exists(xcircrc): + shutil.copy(xcircrc, newproject + '/xcirc/.xcircuitrc') + elif os.path.exists(xcircrc2): + shutil.copy(xcircrc2, newproject + '/xcirc/.xcircuitrc') + + if 'xschem' in schemapps: + xschemrc = newpdk + '/libs.tech/xschem/xschemrc' + if os.path.exists(xschemrc): + shutil.copy(xschemrc, newproject + '/xschem/xschemrc') + + except IOError as e: + print('Error copying files: ' + str(e)) + return None + + return newname + ''' + #---------------------------------------------------------------------- + # Import a CloudV project from ~/cloudv/<project_name> + #---------------------------------------------------------------------- + + def cloudvimport(self, value): + + # Require existing project location + clist = self.get_cloudv_project_list() + if not clist: + return 0 # No projects to import + ppath = ExistingProjectDialog(self, clist, warning="Enter name of cloudV project to import:").result + if not ppath: + return 0 # Canceled in dialog, no action. + pname = os.path.split(ppath)[1] + print("Importing CloudV project " + pname) + + importnode = None + stdcell = None + netlistfile = None + + # Pull process and standard cell library from the YAML file created by + # CloudV. NOTE: YAML file has multiple documents, so must use + # yaml.load_all(), not yaml.load(). If there are refinements of this + # process for individual build files, they will override (see further down). + + # To do: Check entries for SoC builds. If there are multiple SoC builds, + # then create an additional drop-down selection to choose one, since only + # one SoC build can exist as a single Open Galaxy project. Get the name + # of the top-level module for the SoC. (NOTE: It may not be intended + # that there can be multiple SoC builds in the project, so for now retaining + # the existing parsing assuming default names.) + + if os.path.exists(ppath + '/.ef-config/meta.yaml'): + print("Reading YAML file:") + ydicts = [] + with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile: + yalldata = yaml.load_all(ifile, Loader=yaml.Loader) + for ydict in yalldata: + ydicts.append(ydict) + + for ydict in ydicts: + for yentry in ydict.values(): + if 'process' in yentry: + importnode = yentry['process'] + + # If there is a file ().soc and a directory ().model, then pull the file + # ().model/().model.v, which is a chip top-level netlist. + + ydicts = [] + has_soc = False + save_vdir = None + vdirs = glob.glob(ppath + '/*') + for vdir in vdirs: + vnameparts = vdir.split('.') + if len(vnameparts) > 1 and vnameparts[-1] == 'soc' and os.path.isdir(vdir): + has_soc = True + if len(vnameparts) > 1 and vnameparts[-1] == 'model': + save_vdir = vdir + + if has_soc: + if save_vdir: + vdir = save_vdir + print("INFO: CloudV project " + vdir + " is a full chip SoC project.") + + vroot = os.path.split(vdir)[1] + netlistfile = vdir + '/' + vroot + '.v' + if os.path.exists(netlistfile): + print("INFO: CloudV chip top level verilog is " + netlistfile + ".") + else: + print("ERROR: Expected SoC .model directory not found.") + + # Otherwise, if the project has a build/ directory and a netlist.v file, + # then set the foundry node accordingly. + + elif os.path.exists(ppath + '/build'): + vfiles = glob.glob(ppath + '/build/*.v') + for vfile in vfiles: + vroot = os.path.splitext(vfile)[0] + if os.path.splitext(vroot)[1] == '.netlist': + netlistfile = ppath + '/build/' + vfile + + # Pull process and standard cell library from the YAML file + # created by CloudV + # Use yaml.load_all(), not yaml.load() (see above) + + if os.path.exists(ppath + '/.ef-config/meta.yaml'): + print("Reading YAML file:") + ydicts = [] + with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile: + yalldata = yaml.load_all(ifile, Loader=yaml.Loader) + for ydict in yalldata: + ydicts.append(ydict) + + for ydict in ydicts: + for yentry in ydict.values(): + if 'process' in yentry: + importnode = yentry['process'] + if 'stdcell' in yentry: + stdcell = yentry['stdcell'] + break + + if importnode: + print("INFO: Project targets foundry process " + importnode + ".") + else: + print("WARNING: Project does not target any foundry process.") + + newname = self.createproject(value, seedname=pname, importnode=importnode) + if not newname: return 0 # Canceled in dialog, no action. + newpath = self.projectdir + '/' + newname + + result = self.install_from_cloudv(ppath, newpath, importnode, stdcell, ydicts) + if result == None: + print('Error during import.') + return None + elif result == 0: + return 0 # Canceled + else: + return 1 # Success + + #---------------------------------------------------------------------- + # Make a copy of a project in the design folder. + #---------------------------------------------------------------------- + + def copyproject(self, value): + if not value['values']: + print('No project selected.') + return + # Require copy-to location and confirmation + badrex1 = re.compile("^\.") + badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*") + warning = 'Copy project ' + value['text'] + ' to new project.' + print('Copy project directory ' + value['values'][0]) + newname = '' + copylist = [] + elprefs = False + spprefs = False + while True: + copylist = CopyProjectDialog(self, warning, seed=newname).result + if not copylist: + return # Canceled, no action. + else: + newname = copylist[0] + elprefs = copylist[1] + spprefs = copylist[2] + newproject = self.projectdir + '/' + newname + if self.blacklisted(newname): + warning = newname + ' is not allowed for a project name.' + elif badrex1.match(newname): + warning = 'project name may not start with "."' + elif badrex2.match(newname): + warning = 'project name contains illegal characters or whitespace.' + elif os.path.exists(newproject): + warning = newname + ' is already a project name.' + else: + break + + oldpath = value['values'][0] + oldname = os.path.split(oldpath)[1] + patterns = [oldname + '.log'] + if not elprefs: + patterns.append('.java') + if not spprefs: + patterns.append('ngspice') + patterns.append('pv') + + print("New project name will be " + newname) + try: + shutil.copytree(oldpath, newproject, symlinks = True, + ignore = shutil.ignore_patterns(*patterns)) + except IOError as e: + print('Error copying files: ' + str(e)) + return + + # NOTE: Behavior is for project files to depend on "ip-name". Using + # the project filename as a project name is a fallback behavior. If + # there is a project.json file, and it defines an ip-name entry, then + # there is no need to make changes within the project. If there is + # no project.json file, then create one and set the ip-name entry to + # the old project name, which avoids the need to make changes within + # the project. + + else: + # Check project.json + jsonname = newproject + '/project.json' + legacyname = newproject + '/' + oldname + '.json' + if not os.path.isfile(jsonname): + if os.path.isfile(legacyname): + jsonname = legacyname + + found = False + if os.path.isfile(jsonname): + # Pull the ipname into local store (may want to do this with the + # datasheet as well) + with open(jsonname, 'r') as f: + datatop = json.load(f) + dsheet = datatop['data-sheet'] + if 'ip-name' in dsheet: + found = True + + if not found: + jData = self.create_ad_hoc_json(oldname, newproject) + with open(newproject + '/project.json', 'w') as ofile: + json.dump(jData, ofile, indent = 4) + + # If ngspice and electric prefs were not copied from the source + # to the target, as recommended, then copy these from the + # skeleton repository as is done when creating a new project. + + if not spprefs: + try: + os.makedirs(newproject + '/ngspice') + os.makedirs(newproject + '/ngspice/run') + os.makedirs(newproject + '/ngspice/run/.allwaves') + except FileExistsError: + pass + if not elprefs: + # Copy preferences + deskel = '/ef/efabless/deskel' + try: + shutil.copytree(deskel + '/dotjava', newproject + '/elec/.java', symlinks = True) + except IOError as e: + print('Error copying files: ' + e) + + #---------------------------------------------------------------------- + # Change a project IP to a different name. + #---------------------------------------------------------------------- + + def renameproject(self, value): + if not value['values']: + print('No project selected.') + return + + # Require new project name and confirmation + badrex1 = re.compile("^\.") + badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*") + projname = value['text'] + + # Find the IP name for project projname. If it has a JSON file, then + # read it and pull the ip-name record. If not, the fallback position + # is to assume that the project filename is the project name. + + # Check project.json + projectpath = self.projectdir + '/' + projname + jsonname = projectpath + '/project.json' + legacyname = projectpath + '/' + projname + '.json' + if not os.path.isfile(jsonname): + if os.path.isfile(legacyname): + jsonname = legacyname + + oldname = projname + if os.path.isfile(jsonname): + # Pull the ipname into local store (may want to do this with the + # datasheet as well) + with open(jsonname, 'r') as f: + datatop = json.load(f) + dsheet = datatop['data-sheet'] + if 'ip-name' in dsheet: + oldname = dsheet['ip-name'] + + warning = 'Rename IP "' + oldname + '" for project ' + projname + ':' + print(warning) + newname = projname + while True: + try: + newname = ProjectNameDialog(self, warning, seed=oldname + '_1').result + except TypeError: + # TypeError occurs when "Cancel" is pressed, just handle exception. + return None + if not newname: + return None # Canceled, no action. + + if self.blacklisted(newname): + warning = newname + ' is not allowed for an IP name.' + elif badrex1.match(newname): + warning = 'IP name may not start with "."' + elif badrex2.match(newname): + warning = 'IP name contains illegal characters or whitespace.' + else: + break + + # Update everything, including schematic, symbol, layout, JSON file, etc. + print('New project IP name will be ' + newname + '.') + rename_project_all(projectpath, newname) + + # class vars: one-time compile of regulare expressions for life of the process + projNameBadrex1 = re.compile("^[-.]") + projNameBadrex2 = re.compile(".*[][{}()!/ \t\n\\\><#$\*\?\"'|`~]") + importProjNameBadrex1 = re.compile(".*[.]bak$") + + # centralize legal projectName check. + # TODO: Several code sections are not yet converted to use this. + # TODO: Extend to explain to the user the reason why. + def validProjectName(self, name): + return not (self.blacklisted(name) or + self.projNameBadrex1.match(name) or + self.projNameBadrex2.match(name)) + + #---------------------------------------------------------------------- + # "Import As" a dir in import/ as a project. based on renameproject(). + # addWarn is used to augment confirm-dialogue if redirected here via erroneous ImportInto + #---------------------------------------------------------------------- + + def import2project(self, importfile, addWarn=None): + name = os.path.split(importfile)[1] + projpath = self.projectdir + '/' + name + + bakname = name + '.bak' + bakpath = self.projectdir + '/' + bakname + warns = [] + if addWarn: + warns += [ addWarn ] + + # Require new project name and confirmation + confirmPrompt = None # use default: I am sure I want to do this. + if os.path.isdir(projpath): + if warns: + warns += [ '' ] # blank line between addWarn and below two Warnings: + if os.path.isdir(bakpath): + warns += [ 'Warning: Replacing EXISTING: ' + name + ' AND ' + bakname + '!' ] + else: + warns += [ 'Warning: Replacing EXISTING: ' + name + '!' ] + warns += [ 'Warning: Check for & exit any Electric,magic,qflow... for above project(s)!\n' ] + confirmPrompt = 'I checked & exited apps and am sure I want to do this.' + + warns += [ 'Confirm import-as new project: ' + name + '?' ] + warning = '\n'.join(warns) + confirm = ProtectedConfirmDialog(self, warning, confirmPrompt=confirmPrompt).result + if not confirm == 'okay': + return + + print('New project name will be ' + name + '.') + try: + if os.path.isdir(projpath): + if os.path.isdir(bakpath): + print('Deleting old project: ' + bakpath); + shutil.rmtree(bakpath) + print('Moving old project ' + name + ' to ' + bakname) + os.rename( projpath, bakpath) + print("Importing as new project " + name) + os.rename(importfile, projpath) + return True + except IOError as e: + print("Error importing-as project: " + str(e)) + return None + + #---------------------------------------------------------------------- + # Helper subroutine: + # Check if a project is a valid project. Return the name of the + # datasheet if the project has a valid one in the project top level + # path. + #---------------------------------------------------------------------- + + def get_datasheet_name(self, dpath): + if not os.path.isdir(dpath): + print('Error: Project is not a folder!') + return + # Check for valid datasheet name in the following order: + # (1) project.json (Legacy) + # (2) <name of directory>.json (Legacy) + # (3) not "datasheet.json" or "datasheet_anno.json" + # (4) "datasheet.json" + # (5) "datasheet_anno.json" + + dsname = os.path.split(dpath)[1] + if os.path.isfile(dpath + '/project.json'): + datasheet = dpath + '/project.json' + elif os.path.isfile(dpath + '/' + dsname + '.json'): + datasheet = dpath + '/' + dsname + '.json' + else: + has_generic = False + has_generic_anno = False + filelist = os.listdir(dpath) + for file in filelist[:]: + if os.path.splitext(file)[1] != '.json': + filelist.remove(file) + if 'datasheet.json' in filelist: + has_generic = True + filelist.remove('datasheet.json') + if 'datasheet_anno.json' in filelist: + has_generic_anno = True + filelist.remove('datasheet_anno.json') + if len(filelist) == 1: + print('Trying ' + dpath + '/' + filelist[0]) + datasheet = dpath + '/' + filelist[0] + elif has_generic: + datasheet + dpath + '/datasheet.json' + elif has_generic_anno: + datasheet + dpath + '/datasheet_anno.json' + else: + if len(filelist) > 1: + print('Error: Path ' + dpath + ' has ' + str(len(filelist)) + + ' valid datasheets.') + else: + print('Error: Path ' + dpath + ' has no valid datasheets.') + return None + + if not os.path.isfile(datasheet): + print('Error: File ' + datasheet + ' not found.') + return None + else: + return datasheet + + #---------------------------------------------------------------------- + # Run the LVS manager + #---------------------------------------------------------------------- + + def run_lvs(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + designname = self.project_name + print('Run LVS on design ' + designname + ' (' + design + ')') + # use Popen, not run, so that application does not wait for it to exit. + subprocess.Popen(['netgen','-gui',design, designname]) + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the local characterization checker + #---------------------------------------------------------------------- + + def characterize(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + designname = self.project_name + datasheet = self.get_datasheet_name(design) + print('Characterize design ' + designname + ' (' + datasheet + ' )') + if datasheet: + # use Popen, not run, so that application does not wait for it to exit. + dsheetroot = os.path.splitext(datasheet)[0] + subprocess.Popen([config.apps_path + '/cace.py', + datasheet]) + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the local synthesis tool (qflow) + #---------------------------------------------------------------------- + + def synthesize(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + designname = self.project_name + development = self.prefs['devstdcells'] + if not designname: + # A project without a datasheet has no designname (which comes from + # the 'ip-name' record in the datasheet JSON) but can still be + # synthesized. + designname = design + + # Normally there is one digital design in a project. However, full-chip + # designs (in particular) may have multiple sub-projects that are + # independently synthesized digital blocks. Find all subdirectories of + # the top level or subdirectories of qflow that contain a 'qflow_vars.sh' + # file. If there is more than one, then present a list. If there is + # only one but it is not in 'qflow/', then be sure to pass the actual + # directory name to the qflow manager. + + qvlist = glob.glob(design + '/*/qflow_vars.sh') + qvlist.extend(glob.glob(design + '/qflow/*/qflow_vars.sh')) + if len(qvlist) > 1 or (len(qvlist) == 1 and not os.path.exists(design + '/qflow/qflow_vars.sh')): + # Generate selection menu + if len(qvlist) > 1: + clist = list(os.path.split(item)[0] for item in qvlist) + ppath = ExistingProjectDialog(self, clist, warning="Enter name of qflow project to open:").result + if not ppath: + return 0 # Canceled in dialog, no action. + else: + ppath = os.path.split(qvlist[0])[0] + + # pname is everything in ppath after matching design: + pname = ppath.replace(design + '/', '') + + print('Synthesize design in qflow project directory ' + pname) + if development: + subprocess.Popen([config.apps_path + '/qflow_manager.py', + design, '-development', '-subproject=' + pname]) + else: + subprocess.Popen([config.apps_path + '/qflow_manager.py', + design, '-subproject=' + pname]) + else: + print('Synthesize design ' + designname + ' (' + design + ')') + # use Popen, not run, so that application does not wait for it to exit. + if development: + subprocess.Popen([config.apps_path + '/qflow_manager.py', + design, designname, '-development']) + else: + subprocess.Popen([config.apps_path + '/qflow_manager.py', + design, designname]) + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Switch between showing and hiding the import list (default hidden) + #---------------------------------------------------------------------- + + def import_toggle(self): + import_state = self.toppane.import_frame.import_header3.cget('text') + if import_state == '+': + self.importselect.grid(row = 11, sticky = 'news') + self.toppane.import_frame.import_header3.config(text='-') + else: + self.importselect.grid_forget() + self.toppane.import_frame.import_header3.config(text='+') + + def import_open(self): + self.importselect.grid(row = 11, sticky = 'news') + self.toppane.import_frame.import_header3.config(text='-') + + #---------------------------------------------------------------------- + # Switch between showing and hiding the IP library list (default hidden) + #---------------------------------------------------------------------- + + def library_toggle(self): + library_state = self.toppane.library_frame.library_header3.cget('text') + if library_state == '+': + self.ipselect.grid(row = 8, sticky = 'news') + self.toppane.library_frame.library_header3.config(text='-') + else: + self.ipselect.grid_forget() + self.toppane.library_frame.library_header3.config(text='+') + + def library_open(self): + self.ipselect.grid(row = 8, sticky = 'news') + self.toppane.library_frame.library_header3.config(text='-') + + #---------------------------------------------------------------------- + # Run padframe-calc (today internally invokes libreoffice, we only need cwd set to design project) + #---------------------------------------------------------------------- + def padframe_calc(self): + value = self.projectselect.selected() + if value: + designname = self.project_name + self.padframe_calc_work(newname=designname) + else: + print("You must first select a project.", file=sys.stderr) + + #------------------------------------------------------------------------ + # Run padframe-calc (today internally invokes libreoffice, we set cwd to design project) + # Modelled somewhat after 'def importvgl': + # Prompt for an existing electric lib. + # Prompt for a target cellname (for both mag and electric icon). + # (The AS vs INTO behavior is incomplete as yet. Used so far with current-project as newname arg). + # newname : target project-name (INTO), or None (AS: i.e. prompt to create one). + # Either newname is given: we PROMPT to pick an existing elecLib; + # Else PROMPT for new projectName and CREATE it (and use elecLib of same name). + #------------------------------------------------------------------------ + def padframe_calc_work(self, newname=None): + elecLib = newname + isnew = not newname + if isnew: + # Use create project code first to generate a valid project space. + newname = self.createproject(None) + if not newname: return 0 # Canceled in dialog, no action. + # print("padframe-calc in new project " + newname + ".") + elecLib = newname + + # For life of this projectManager process, store/recall last PadFrame Settings per project + global project2pfd + try: + project2pfd + except: + project2pfd = {} + if newname not in project2pfd: + project2pfd[newname] = {"libEntry": None, "cellName": None} + + ppath = self.projectdir + '/' + newname + choices = self.get_elecLib_list(newname) + if not choices: + print( "Aborted: No existing electric libraries found to write symbol into.") + return 0 + + elecLib = newname + '/elec/' + elecLib + '.delib' + elecLib = project2pfd[newname]["libEntry"] or elecLib + cellname = project2pfd[newname]["cellName"] or "padframe" + libAndCell = ExistingElecLibCellDialog(self, None, title="PadFrame Settings", plist=choices, descPost="of icon&layout", seedLibNm=elecLib, seedCellNm=cellname).result + if not libAndCell: + return 0 # Canceled in dialog, no action. + + (elecLib, cellname) = libAndCell + if not cellname: + return 0 # empty cellname, no action. + + project2pfd[newname]["libEntry"] = elecLib + project2pfd[newname]["cellName"] = cellname + + # Isolate just electric lib name without extension. ../a/b.delib -> b + elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0] + print("padframe-calc in project: %s, elecLib: %s, cellName: %s" % (newname, elecLib, cellname)) + + export = dict(os.environ) + export['EF_DESIGNDIR'] = ppath + subprocess.Popen(['/ef/apps/bin/padframe-calc', elecLib, cellname], cwd = ppath, env = export) + + # not yet any useful return value or reporting of results here in projectManager... + return 1 + + #---------------------------------------------------------------------- + # Run the schematic editor (tool as given by user preference) + #---------------------------------------------------------------------- + + def edit_schematic(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + + pdktechdir = design + self.config_path(design)+'/techdir/libs.tech' + + applist = self.list_valid_schematic_editors(pdktechdir) + + if len(applist)==0: + print("Unable to find a valid schematic editor.") + return + + # If the preferred app is in the list, then use it. + + if self.prefs['schemeditor'] in applist: + appused = self.prefs['schemeditor'] + else: + appused = applist[0] + + if appused == 'xcircuit': + return self.edit_schematic_with_xcircuit() + elif appused == 'xschem': + return self.edit_schematic_with_xschem() + elif appused == 'electric': + return self.edit_schematic_with_electric() + else: + print("Unknown/unsupported schematic editor " + appused + ".", file=sys.stderr) + + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the schematic editor (electric) + #---------------------------------------------------------------------- + + def edit_schematic_with_electric(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + # self.project_name set by setcurrent. This is the true project + # name, as opposed to the directory name. + designname = self.project_name + print('Edit schematic ' + designname + ' (' + design + ' )') + # Collect libs on command-line; electric opens these in Explorer + libs = [] + ellibrex = re.compile(r'^(tech_.*|ef_examples)\.[dj]elib$', re.IGNORECASE) + + self.reinitElec(design) + + # /elec and /.java are prerequisites for running electric + if not os.path.exists(design + '/elec'): + print("No path to electric design folder.") + return + + if not os.path.exists(design + '/elec/.java'): + print("No path to electric .java folder.") + return + + # Fix the LIBDIRS file if needed + #fix_libdirs(design, create = True) + + # Check for legacy directory (missing .ef-config and/or .ef-config/techdir); + # Handle as necessary. + + # don't sometimes yield pdkdir as some subdir of techdir + pdkdir = design + self.config_path(design) + '/techdir/' + if not os.path.exists(pdkdir): + export = dict(os.environ) + export['EF_DESIGNDIR'] = design + ''' + p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'], + stdout = subprocess.PIPE, env = export) + config_out = p.stdout.splitlines() + for line in config_out: + setline = line.decode('utf-8').split('=') + if setline[0] == 'EF_TECHDIR': + pdkdir = re.sub("[';]", "", setline[1]) + ''' + + for subpath in ('libs.tech/elec/', 'libs.ref/elec/'): + pdkelec = os.path.join(pdkdir, subpath) + if os.path.exists(pdkelec) and os.path.isdir(pdkelec): + # don't use os.walk(), it is recursive, wastes time + for entry in os.scandir(pdkelec): + if ellibrex.match(entry.name): + libs.append(entry.path) + + # Locate most useful project-local elec-lib to open on electric cmd-line. + designroot = os.path.split(design)[1] + finalInDesDirLibAdded = False + if os.path.exists(design + '/elec/' + designname + '.jelib'): + libs.append(design + '/elec/' + designname + '.jelib') + finalInDesDirLibAdded = True + elif os.path.isdir(design + '/elec/' + designname + '.delib'): + libs.append(design + '/elec/' + designname + '.delib') + finalInDesDirLibAdded = True + else: + # Alternative path is the project name + .delib + if os.path.isdir(design + '/elec/' + designroot + '.delib'): + libs.append(design + '/elec/' + designroot + '.delib') + finalInDesDirLibAdded = True + + # Finally, check for the one absolute requirement for a project, + # which is that there must be a symbol designname + .ic in the + # last directory. If not, then do a search for it. + if not finalInDesDirLibAdded or not os.path.isfile(libs[-1] + '/' + designname + '.ic'): + delibdirs = os.listdir(design + '/elec') + for delibdir in delibdirs: + if os.path.splitext(delibdir)[1] == '.delib': + iconfiles = os.listdir(design + '/elec/' + delibdir) + for iconfile in iconfiles: + if iconfile == designname + '.ic': + libs.append(design + '/elec/' + delibdir) + finalInDesDirLibAdded = True + break + + # Above project-local lib-adds are all conditional on finding some lib + # with an expected name or content: all of which may fail. + # Force last item ALWAYS to be 'a path' in the project's elec/ dir. + # Usually it's a real library (found above). (If lib does not exist the messages + # window does get an error message). But the purpose is for the universal side-effect: + # To EVERY TIME reseed the File/OpenLibrary dialogue WorkDir to start in + # project's elec/ dir; avoid it starting somewhere in the PDK, which + # is what will happen if last actual cmd-line arg is a lib in the PDK, and + # about which users have complained. (Optimal fix needs electric enhancement). + if not finalInDesDirLibAdded: + libs.append(design + '/elec/' + designroot + '.delib') + + # Pull last item from libs and make it a command-line argument. + # All other libraries become part of the EOPENARGS environment variable, + # and electric is called with the elecOpen.bsh script. + indirectlibs = libs[:-1] + export = dict(os.environ) + arguments = [] + if indirectlibs: + export['EOPENARGS'] = ' '.join(indirectlibs) + arguments.append('-s') + arguments.append('/ef/efabless/lib/elec/elecOpen.bsh') + + try: + arguments.append(libs[-1]) + except IndexError: + print('Error: Electric project directories not set up correctly?') + else: + subprocess.Popen(['electric', *arguments], cwd = design + '/elec', + env = export) + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the schematic editor (xcircuit) + #---------------------------------------------------------------------- + + def edit_schematic_with_xcircuit(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + # self.project_name set by setcurrent. This is the true project + # name, as opposed to the directory name. + designname = self.project_name + print('Edit schematic ' + designname + ' (' + design + ' )') + xcircdirpath = design + '/xcirc' + pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xcircuit' + + # /xcirc directory is a prerequisite for running xcircuit. If it doesn't + # exist, create it and seed it with .xcircuitrc from the tech directory + if not os.path.exists(xcircdirpath): + os.makedirs(xcircdirpath) + + # Copy xcircuit startup file from tech directory + hasxcircrcfile = os.path.exists(xcircdirpath + '/.xcircuitrc') + if not hasxcircrcfile: + if os.path.exists(pdkdir + '/xcircuitrc'): + shutil.copy(pdkdir + '/xcircuitrc', xcircdirpath + '/.xcircuitrc') + + # Command line argument is the project name + arguments = [design + '/xcirc' + designname] + subprocess.Popen(['xcircuit', *arguments]) + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the schematic editor (xschem) + #---------------------------------------------------------------------- + + def edit_schematic_with_xschem(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # self.project_name set by setcurrent. This is the true project + # name, as opposed to the directory name. + designname = self.project_name + print('Edit schematic ' + designname + ' (' + design + ' )') + xschemdirpath = design + '/xschem' + + pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xschem' + + + # /xschem directory is a prerequisite for running xschem. If it doesn't + # exist, create it and seed it with xschemrc from the tech directory + if not os.path.exists(xschemdirpath): + os.makedirs(xschemdirpath) + + # Copy xschem startup file from tech directory + hasxschemrcfile = os.path.exists(xschemdirpath + '/xschemrc') + if not hasxschemrcfile: + if os.path.exists(pdkdir + '/xschemrc'): + shutil.copy(pdkdir + '/xschemrc', xschemdirpath + '/xschemrc') + + # Command line argument is the project name. The "-r" option is recommended if there + # is no stdin/stdout piping. + + arguments = ['-r', design + '/xschem/' + designname] + subprocess.Popen(['xschem', *arguments]) + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the layout editor (magic or klayout) + #---------------------------------------------------------------------- + + def edit_layout(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + pdktechdir = design + self.config_path(design) + '/techdir/libs.tech' + + applist = self.list_valid_layout_editors(pdktechdir) + + if len(applist)==0: + print("Unable to find a valid layout editor.") + return + + # If the preferred app is in the list, then use it. + if self.prefs['layouteditor'] in applist: + appused = self.prefs['layouteditor'] + else: + appused = applist[0] + + if appused == 'magic': + return self.edit_layout_with_magic() + elif appused == 'klayout': + return self.edit_layout_with_klayout() + elif appused == 'electric': + return self.edit_layout_with_electric() + else: + print("Unknown/unsupported layout editor " + appused + ".", file=sys.stderr) + + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the magic layout editor + #---------------------------------------------------------------------- + + def edit_layout_with_magic(self): + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + designname = self.project_name + + pdkdir = '' + pdkname = '' + + if os.path.exists(design + '/.ef-config/techdir/libs.tech'): + pdkdir = design + '/.ef-config/techdir/libs.tech/magic/current' + pdkname = os.path.split(os.path.realpath(design + '/.ef-config/techdir'))[1] + elif os.path.exists(design + '/.config/techdir/libs.tech'): + pdkdir = design + '/.config/techdir/libs.tech/magic' + pdkname = os.path.split(os.path.realpath(design + '/.config/techdir'))[1] + + + # Check if the project has a /mag directory. Create it and + # put the correct .magicrc file in it, if it doesn't. + magdirpath = design + '/mag' + hasmagdir = os.path.exists(magdirpath) + if not hasmagdir: + os.makedirs(magdirpath) + + hasmagrcfile = os.path.exists(magdirpath + '/.magicrc') + if not hasmagrcfile: + shutil.copy(pdkdir + '/' + pdkname + '.magicrc', magdirpath + '/.magicrc') + + # Check if the .mag file exists for the project. If not, + # generate a dialog. + magpath = design + '/mag/' + designname + '.mag' + netpath = design + '/spi/' + designname + '.spi' + # print("magpath is " + magpath) + hasmag = os.path.exists(magpath) + hasnet = os.path.exists(netpath) + if hasmag: + if hasnet: + statbuf1 = os.stat(magpath) + statbuf2 = os.stat(netpath) + # No specific action for out-of-date layout. To be done: + # Check contents and determine if additional devices need to + # be added to the layout. This may be more trouble than it's + # worth. + # + # if statbuf2.st_mtime > statbuf1.st_mtime: + # hasmag = False + + if not hasmag: + # Does the project have any .mag files at all? If so, the project + # layout may be under a name different than the project name. If + # so, present the user with a selectable list of layout names, + # with the option to start a new layout or import from schematic. + + maglist = os.listdir(design + '/mag/') + if len(maglist) > 1: + # Generate selection menu + warning = 'No layout matches IP name ' + designname + '.' + maglist = list(item for item in maglist if os.path.splitext(item)[1] == '.mag') + clist = list(os.path.splitext(item)[0] for item in maglist) + ppath = EditLayoutDialog(self, clist, ppath=design, + pname=designname, warning=warning, + hasnet=hasnet).result + if not ppath: + return 0 # Canceled in dialog, no action. + elif ppath != '(New layout)': + hasmag = True + designname = ppath + elif len(maglist) == 1: + # Only one magic file, no selection, just bring it up. + designname = os.path.split(maglist[0])[1] + hasmag = True + + if not hasmag: + populate = NewLayoutDialog(self, "No layout for project.").result + if not populate: + return 0 # Canceled, no action. + elif populate(): + # Name of PDK deprecated. The .magicrc file in the /mag directory + # will load the correct PDK and specify the proper library for the + # low-level device namespace, which may not be the same as techdir. + # NOTE: netlist_to_layout script will attempt to generate a + # schematic netlist if one does not exist. + + print('Running /ef/efabless/bin/netlist_to_layout.py ../spi/' + designname + '.spi') + try: + p = subprocess.run(['/ef/efabless/bin/netlist_to_layout.py', + '../spi/' + designname + '.spi'], + stdin = subprocess.PIPE, stdout = subprocess.PIPE, + stderr = subprocess.PIPE, cwd = design + '/mag') + if p.stderr: + err_string = p.stderr.splitlines()[0].decode('utf-8') + # Print error messages to console + print(err_string) + + except subprocess.CalledProcessError as e: + print('Error running netlist_to_layout.py: ' + e.output.decode('utf-8')) + else: + if os.path.exists(design + '/mag/create_script.tcl'): + with open(design + '/mag/create_script.tcl', 'r') as infile: + magproc = subprocess.run(['/ef/apps/bin/magic', + '-dnull', '-noconsole', '-rcfile ', + pdkdir + '/' + pdkname + '.magicrc', designname], + stdin = infile, stdout = subprocess.PIPE, + stderr = subprocess.PIPE, cwd = design + '/mag') + print("Populated layout cell") + # os.remove(design + '/mag/create_script.tcl') + else: + print("No device generating script was created.", file=sys.stderr) + + print('Edit layout ' + designname + ' (' + design + ' )') + + magiccommand = ['magic'] + # Select the graphics package used by magic from the profile settings. + if 'magic-graphics' in self.prefs: + magiccommand.extend(['-d', self.prefs['magic-graphics']]) + # Check if .magicrc predates the latest and warn if so. + statbuf1 = os.stat(design + '/mag/.magicrc') + statbuf2 = os.stat(pdkdir + '/' + pdkname + '.magicrc') + if statbuf2.st_mtime > statbuf1.st_mtime: + print('NOTE: File .magicrc predates technology startup file. Using default instead.') + magiccommand.extend(['-rcfile', pdkdir + '/' + pdkname + '.magicrc']) + magiccommand.append(designname) + + # Run magic and don't wait for it to finish + subprocess.Popen(magiccommand, cwd = design + '/mag') + else: + print("You must first select a project.", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the klayout layout editor + #---------------------------------------------------------------------- + + def edit_layout_with_klayout(self): + value = self.projectselect.selected() + print("Klayout unsupported from project manager (work in progress); run manually", file=sys.stderr) + + #---------------------------------------------------------------------- + # Run the electric layout editor + #---------------------------------------------------------------------- + + def edit_layout_with_electric(self): + value = self.projectselect.selected() + print("Electric layout editing unsupported from project manager (work in progress); run manually", file=sys.stderr) + + #---------------------------------------------------------------------- + # Upload design to the marketplace + # NOTE: This is not being called by anything. Use version in the + # characterization script, which can check for local results before + # approving (or forcing) an upload. + #---------------------------------------------------------------------- + + def upload(self): + ''' + value = self.projectselect.selected() + if value: + design = value['values'][0] + # designname = value['text'] + designname = self.project_name + print('Upload design ' + designname + ' (' + design + ' )') + subprocess.run(['/ef/apps/bin/withnet', + og_config.apps_path + '/cace_design_upload.py', + design, '-test']) + ''' + + #-------------------------------------------------------------------------- + # Upload a datasheet to the marketplace (Administrative use only, for now) + #-------------------------------------------------------------------------- + + # def make_challenge(self): + # importp = self.cur_import + # print("Make a Challenge from import " + importp + "!") + # # subprocess.run([og_config.apps_path + '/cace_import_upload.py', importp, '-test']) + + def setcurrent(self, value): + global currdesign + treeview = value.widget + selection = treeview.item(treeview.selection()) + pname = selection['text'] + #print("setcurrent returned value " + pname) + efmetapath = os.path.expanduser(currdesign) + if not os.path.exists(efmetapath): + os.makedirs(os.path.split(efmetapath)[0], exist_ok=True) + with open(efmetapath, 'w') as f: + f.write(pname + '\n') + + # Pick up the PDK from "values", use it to find the PDK folder, determine + # if it has a "magic" subfolder, and enable/disable the "Edit Layout" + # button accordingly + + svalues = selection['values'] + pdkitems = svalues[1].split() + pdkdir = '' + + ef_style=False + + if os.path.exists(svalues[0] + '/.config'): + pdkdir = svalues[0] + '/.config/techdir' + elif os.path.exists(svalues[0] + '/.ef-config'): + pdkdir = svalues[0] + '/.ef-config/techdir' + ef_style=True + + if pdkdir == '': + print('No pdkname found; layout editing disabled') + self.toppane.appbar.layout_button.config(state='disabled') + else: + try: + if ef_style: + subf = os.listdir(pdkdir + '/libs.tech/magic/current') + else: + subf = os.listdir(pdkdir + '/libs.tech/magic') + except: + print('PDK ' + pdkname + ' has no layout setup; layout editing disabled') + self.toppane.appbar.layout_button.config(state='disabled') + ''' + svalues = selection['values'][1] + print('selection: '+str(selection)) + pdkitems = svalues.split() + print('Contents of pdkitems: '+str(pdkitems)) + pdkname = '' + if ':' in pdkitems: + pdkitems.remove(':') + if len(pdkitems) == 2: + # New behavior Sept. 2017, have to cope with <foundry>.<N> directories, ugh. + pdkdirs = os.listdir('/usr/share/pdk/') + #TODO: pdkdirs = os.listdir('PREFIX/pdk/') + + for pdkdir in pdkdirs: + if pdkitems[0] == pdkdir: + pdkname = pdkdir + #TODO: PREFIX + if os.path.exists('/usr/share/pdk/' + pdkname + '/' + pdkitems[1]): + break + else: + pdkpair = pdkdir.split('.') + if pdkpair[0] == pdkitems[0]: + pdkname = pdkdir + #TODO: PREFIX + if os.path.exists('/usr/share/pdk/' + pdkname + '/' + pdkitems[1]): + break + if pdkname == '': + print('No pdkname found; layout editing disabled') + self.toppane.appbar.layout_button.config(state='disabled') + else: + try: + subf = os.listdir('/ef/tech/' + pdkname + '/' + pdkitems[1] + '/libs.tech/magic/current') + except: + print('PDK ' + pdkname + ' has no layout setup; layout editing disabled') + self.toppane.appbar.layout_button.config(state='disabled') + else: + self.toppane.appbar.layout_button.config(state='enabled') + else: + print('No PDK returned in project selection data; layout editing disabled.') + self.toppane.appbar.layout_button.config(state='disabled') + ''' + # If the selected project directory has a JSON file and netlists in the "spi" + # and "testbench" folders, then enable the "Characterize" button; else disable + # it. + # NOTE: project.json is the preferred name for the datasheet + # file. However, the .spi file, .delib file, etc., all have the name of the + # project from "ip-name" in the datasheet. + # "<project_folder_name>.json" is the legacy name for the datasheet, deprecated. + + found = False + ppath = selection['values'][0] + jsonname = ppath + '/project.json' + legacyname = ppath + '/' + pname + '.json' + if not os.path.isfile(jsonname): + if os.path.isfile(legacyname): + jsonname = legacyname + + if os.path.isfile(jsonname): + # Pull the ipname into local store (may want to do this with the + # datasheet as well) + with open(jsonname, 'r') as f: + datatop = json.load(f) + dsheet = datatop['data-sheet'] + ipname = dsheet['ip-name'] + self.project_name = ipname + found = True + + # Do not specifically prohibit opening the characterization app if + # there is no schematic or netlist. Otherwise the user is prevented + # even from seeing the electrical parameters. Let the characterization + # tool allow or prohibit simulation based on this. + # if os.path.exists(ppath + '/spi'): + # if os.path.isfile(ppath + '/spi/' + ipname + '.spi'): + # found = True + # + # if found == False and os.path.exists(ppath + '/elec'): + # if os.path.isdir(ppath + '/elec/' + ipname + '.delib'): + # if os.path.isfile(ppath + '/elec/' + ipname + '.delib/' + ipname + '.sch'): + # found = True + else: + # Use 'pname' as the default project name. + print('No characterization file ' + jsonname) + print('Setting project ip-name from the project folder name.') + self.project_name = pname + + # If datasheet has physical parameters but not electrical parameters, then it's okay + # for it not to have a testbench directory; it's still valid. However, having + # neither physical nor electrical parameters means there's nothing to characterize. + if found and 'electrical-params' in dsheet and len(dsheet['electrical-params']) > 0: + if not os.path.isdir(ppath + '/testbench'): + print('No testbench directory for eletrical parameter simulation methods.', file=sys.stderr) + found = False + elif found and not 'physical-params' in dsheet: + print('Characterization file defines no characterization tests.', file=sys.stderr) + found = False + elif found and 'physical-params' in dsheet and len(dsheet['physical-params']) == 0: + print('Characterization file defines no characterization tests.', file=sys.stderr) + found = False + + if found == True: + self.toppane.appbar.char_button.config(state='enabled') + else: + self.toppane.appbar.char_button.config(state='disabled') + + # Warning: temporary hack (Tim, 1/9/2018) + # Pad frame generator is currently limited to the XH035 cells, so if the + # project PDK is not XH035, disable the pad frame button + + if len(pdkitems) > 1 and pdkitems[1] == 'EFXH035B': + self.toppane.appbar.padframeCalc_button.config(state='enabled') + else: + self.toppane.appbar.padframeCalc_button.config(state='disabled') + +# main app. fyi: there's a 2nd/earlier __main__ section for splashscreen +if __name__ == '__main__': + OpenGalaxyManager(root) + if deferLoad: + # Without this, mainloop may find&run very short clock-delayed events BEFORE main form display. + # With it 1st project-load can be scheduled using after-time=0 (needn't tune a delay like 100ms). + root.update_idletasks() + root.mainloop()