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()