| #!/usr/bin/env python3 |
| # |
| #-------------------------------------------------------- |
| # Efabless Open Galaxy Project Manager GUI ported to |
| # open_pdks. |
| # |
| # This is a Python tkinter script that handles local |
| # project management. |
| # |
| #-------------------------------------------------------- |
| # Written by Tim Edwards |
| # efabless, inc. |
| # September 9, 2016 |
| # Modifications 2017, 2018 |
| # Updates 2021 by Max Chen |
| # Version 1.1 |
| #-------------------------------------------------------- |
| |
| import sys |
| import io |
| import os |
| import re |
| import glob |
| import json |
| import signal |
| import shutil |
| import tarfile |
| import datetime |
| import tempfile |
| import subprocess |
| import contextlib |
| import faulthandler |
| |
| import tkinter |
| from tkinter import ttk, StringVar, Listbox, END |
| from tkinter import filedialog |
| |
| # Local imports |
| |
| import tksimpledialog |
| import tooltip |
| |
| from rename_project import rename_project_all |
| 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 |
| |
| # Global variables |
| |
| theProg = sys.argv[0] |
| |
| deferLoad = True # True: display GUI before (slow) loading of projects |
| |
| # There must be exactly one instance of Tk; don't call again elsewhere. |
| root = tkinter.Tk() |
| |
| # Name for design directory |
| designdir = 'design' |
| |
| # Name for import directory |
| importdir = 'import' |
| |
| # Name for cloudv directory |
| cloudvdir = 'cloudv' |
| |
| # Name for archived imports project sub-directory |
| archiveimportdir = 'imported' |
| |
| # Name for current design file |
| currdesign = '~/.open_pdks/currdesign' |
| |
| # Name for personal preferences file |
| prefsfile = '~/.open_pdks/prefs.json' |
| |
| # Application directory. |
| try: |
| pdk_root = os.environ('PDK_ROOT') |
| except: |
| pdk_root = 'PREFIX/pdk' |
| |
| apps_path = pdk_root + '/scripts' |
| |
| #--------------------------------------------------------------- |
| # 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, parent_pdk=''): |
| global pdk_root |
| 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 |
| for pdkdir_lr in glob.glob(pdk_root + '/*/libs.tech/'): |
| pdkdir = os.path.split( os.path.split( pdkdir_lr )[0])[0] # discard final .../libs.tech/ |
| (foundry, foundry_name, node, desc, status) = ProjectManager.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]) |
| |
| if parent_pdk != '': |
| pdk_def = parent_pdk |
| |
| 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 or parent_pdk != '': |
| 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): |
| 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' |
| |
| #------------------------------------------------------ |
| # Dialog to import a project into the project manager |
| #------------------------------------------------------ |
| |
| class ImportDialog(tksimpledialog.Dialog): |
| def body(self, master, warning, seed, parent_pdk, parent_path, project_dir): |
| self.badrex1 = re.compile("^\.") |
| self.badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*") |
| |
| self.projectpath = "" |
| self.project_pdkdir = "" |
| self.foundry = "" |
| self.node = "" |
| self.parentpdk = parent_pdk |
| self.parentpath = parent_path |
| self.projectdir = project_dir #folder that contains all projects |
| |
| if warning: |
| ttk.Label(master, text=warning, wraplength=250).grid(row = 0, columnspan = 2, sticky = 'wns') |
| ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0) |
| |
| self.entry_v = tkinter.StringVar() |
| |
| self.nentry = ttk.Entry(master, textvariable = self.entry_v) |
| self.nentry.grid(row = 1, column = 1, sticky = 'ewns') |
| |
| self.entry_v.trace('w', self.text_validate) |
| |
| |
| ttk.Button(master, |
| text = "Choose Project...", |
| command = self.browseFiles).grid(row = 3, column = 0) |
| |
| self.pathlabel = ttk.Label(master, text = ("No project selected" if self.projectpath =="" else self.projectpath), style = 'red.TLabel', wraplength=300) |
| |
| self.pathlabel.grid(row = 3, column = 1) |
| |
| ttk.Label(master, text="Foundry/node:").grid(row = 4, column = 0) |
| |
| self.pdklabel = ttk.Label(master, text="N/A", style = 'red.TLabel') |
| self.pdklabel.grid(row = 4, column = 1) |
| |
| self.importoption = tkinter.StringVar() |
| |
| self.importoption.set(("copy" if parent_pdk!='' else "link")) |
| |
| self.linkbutton = ttk.Radiobutton(master, text="Make symbolic link", variable=self.importoption, value="link") |
| self.linkbutton.grid(row = 5, column = 0) |
| ttk.Radiobutton(master, text="Copy project", variable=self.importoption, value="copy").grid(row = 5, column = 1) |
| |
| self.error_label = ttk.Label(master, text="", style = 'red.TLabel', wraplength=300) |
| self.error_label.grid(row = 6, column = 0, columnspan = 2) |
| |
| self.entry_error = ttk.Label(master, text="", style = 'red.TLabel', wraplength=300) |
| self.entry_error.grid(row = 2, column = 0, columnspan = 2) |
| |
| return self.nentry |
| |
| def text_validate(self, *args): |
| newname = self.entry_v.get() |
| projectpath = '' |
| if self.parentpath!='': |
| projectpath = self.parentpath + '/subcells/' + newname |
| else: |
| projectpath = self.projectdir + '/' + newname |
| |
| if ProjectManager.blacklisted( newname): |
| self.entry_error.configure(text = newname + ' is not allowed for a project name.') |
| elif newname == "": |
| self.entry_error.configure(text = "") |
| elif self.badrex1.match(newname): |
| self.entry_error.configure(text = 'project name may not start with "."') |
| elif self.badrex2.match(newname): |
| self.entry_error.configure(text = 'project name contains illegal characters or whitespace.') |
| elif os.path.exists(projectpath): |
| self.entry_error.configure(text = newname + ' is already a project name.') |
| else: |
| self.entry_error.configure(text = '') |
| return True |
| return False |
| |
| def validate(self, *args): |
| return self.text_validate(self) and self.pdk_validate(self) |
| |
| def browseFiles(self): |
| initialdir = "~/" |
| if os.path.isdir(self.projectpath): |
| initialdir = os.path.split(self.projectpath)[0] |
| |
| selected_dir = filedialog.askdirectory(initialdir = initialdir, title = "Select a Project to Import",) |
| |
| if os.path.isdir(str(selected_dir)): |
| self.error_label.configure(text = '') |
| self.linkbutton.configure(state="normal") |
| |
| self.projectpath = selected_dir |
| self.pathlabel.configure(text=self.projectpath, style = 'blue.TLabel') |
| # Change label contents |
| if (self.nentry.get() == ''): |
| self.nentry.insert(0, os.path.split(self.projectpath)[1]) |
| |
| self.pdk_validate(self) |
| |
| def pdk_validate(self, *args): |
| if not os.path.exists(self.projectpath): |
| self.error_label.configure(text = 'Invalid directory') |
| return False |
| |
| if self.parentpath != "" and self.projectpath in self.parentpath: |
| self.error_label.configure(text = 'Cannot import a parent directory into itself.') |
| return False |
| #Find project pdk |
| if os.path.exists(self.projectpath + '/.config/techdir') or os.path.exists(self.projectpath + '/.ef-config/techdir'): |
| self.project_pdkdir = os.path.realpath(self.projectpath + ProjectManager.config_path( self.projectpath) + '/techdir') |
| self.foundry, foundry_name, self.node, desc, status = ProjectManager.pdkdir2fnd( self.project_pdkdir ) |
| else: |
| if not os.path.exists(self.projectpath + '/project.json'): |
| self.error_label.configure(text = self.projectpath + ' does not contain an project.json file.') |
| self.project_pdkdir = "" |
| self.foundry = "" |
| self.node = "" |
| else: |
| self.project_pdkdir, self.foundry, self.node = ProjectManager.get_import_pdk( self.projectpath) |
| |
| if self.project_pdkdir == "": |
| self.pdklabel.configure(text="Not found", style='red.TLabel') |
| return False |
| else: |
| if (self.parentpdk!="" and self.parentpdk != self.foundry + '/' + self.node): |
| self.importoption.set("copy") |
| self.linkbutton.configure(state="disabled") |
| self.error_label.configure(text = 'Warning: Parent project uses '+self.parentpdk+' instead of '+self.foundry + '/' + self.node+'. The imported project will be copied and cleaned.') |
| self.pdklabel.configure(text=self.foundry + '/' + self.node, style='blue.TLabel') |
| return True |
| |
| |
| def apply(self): |
| return self.nentry.get(), self.project_pdkdir, self.projectpath, self.importoption.get() |
| |
| #------------------------------------------------------ |
| # Dialog to allow users to select a flow |
| #------------------------------------------------------ |
| |
| class SelectFlowDialog(tksimpledialog.Dialog): |
| def body(self, master, warning, seed='', is_subproject = False): |
| self.wait_visibility() |
| if warning: |
| ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns') |
| |
| ttk.Label(master, text="Flow:").grid(row = 1, column = 0) |
| |
| project_flows = { |
| 'Analog':'Schematic, Simulation, Layout, DRC, LVS', |
| 'Digital':'Preparation, Synthesis, Placement, Static Timing Analysis, Routing, Post-Route STA, Migration, DRC, LVS, GDS, Cleanup', |
| 'Mixed-Signal':'', |
| 'Assembly':'', |
| } |
| |
| subproject_flows = { |
| 'Analog':'Schematic, Simulation, Layout, DRC, LVS', |
| 'Digital':'Preparation, Synthesis, Placement, Static Timing Analysis, Routing, Post-Route STA, Migration, DRC, LVS, GDS, Cleanup', |
| 'Mixed-Signal': '', |
| } |
| self.flows = subproject_flows if is_subproject else project_flows |
| self.flowvar = tkinter.StringVar(master, value = 'Analog') |
| |
| self.infolabel = ttk.Label(master, text=self.flows[self.flowvar.get()], style = 'brown.TLabel', wraplength=250) |
| self.infolabel.grid(row = 2, column = 0, columnspan = 2, sticky = 'news') |
| |
| self.option_menu = ttk.OptionMenu( |
| master, |
| self.flowvar, |
| self.flowvar.get(), |
| *self.flows.keys(), |
| command=self.show_info |
| ) |
| |
| self.option_menu.grid(row = 1, column = 1) |
| |
| return self.option_menu# initial focus |
| |
| def show_info(self, args): |
| key = self.flowvar.get() |
| print(key) |
| desc = self.flows[key] |
| if desc == '': |
| self.infolabel.config(text='(no description available)') |
| else: |
| self.infolabel.config(text=desc) |
| |
| |
| def apply(self): |
| return str(self.flowvar.get()) # Note converts StringVar to string |
| |
| #------------------------------------------------------ |
| # Project Manager class |
| #------------------------------------------------------ |
| |
| class ProjectManager(ttk.Frame): |
| """Project Management GUI.""" |
| |
| def __init__(self, parent, *args, **kwargs): |
| super().__init__(parent, *args, **kwargs) |
| self.root = parent |
| parent.withdraw() |
| self.update_idletasks() # erase small initial frame asap |
| self.init_gui() |
| parent.protocol("WM_DELETE_WINDOW", self.on_quit) |
| 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 |
| global apps_path |
| |
| message = [] |
| allPaneOpen = False |
| prjPaneMinh = 10 |
| iplPaneMinh = 4 |
| impPaneMinh = 4 |
| |
| # 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(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('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') |
| |
| 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 ~/.open_pdks/currdesign and set the selection. |
| try: |
| with open(os.path.expanduser(currdesign), 'r') as f: |
| pdirCur = f.read().rstrip() |
| except: |
| pdirCur = 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 = pdirCur, natSort = True) |
| self.projectselect.populate("Available Projects:", projectlist, |
| [["New", True, self.createproject], |
| ["Import", True, self.importproject], |
| ["Flow", False, self.startflow], |
| ["Copy", False, self.copyproject], |
| ["Rename", False, self.renameproject], |
| ["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/subproject") |
| tooltip.ToolTip(self.projectselect.get_button(1), text="Import a project/subproject") |
| tooltip.ToolTip(self.projectselect.get_button(2), text="Start design flow") |
| tooltip.ToolTip(self.projectselect.get_button(3), text="Make a copy of an entire project") |
| tooltip.ToolTip(self.projectselect.get_button(4), text="Rename a project folder") |
| 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 pdirCur: |
| try: |
| curitem = next(item for item in projectlist if pdirCur == item) |
| except StopIteration: |
| pass |
| else: |
| if curitem: |
| self.projectselect.setselect(pdirCur) |
| |
| # 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") |
| |
| @classmethod |
| def config_path(cls, path): |
| #returns the config 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 Exception('Neither '+path+'/.config nor '+path+'/.ef-config exists.') |
| |
| #------------------------------------------------------------------------ |
| # Check if a name is blacklisted for being a project folder |
| #------------------------------------------------------------------------ |
| |
| @classmethod |
| def blacklisted(cls, dirname): |
| # Blacklist: Do not show files of these names: |
| blacklist = [importdir, 'ip', 'upload', 'export', 'lost+found', 'subcells'] |
| 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 |
| global apps_path |
| |
| # 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' , |
| 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 = [] |
| |
| def add_projects(projectpath): |
| # Recursively add subprojects to projectlist |
| projectlist.append(projectpath) |
| # Check for subprojects and add them |
| if os.path.isdir(projectpath + '/subcells'): |
| for subproj in os.listdir(projectpath + '/subcells'): |
| if os.path.isdir(projectpath + '/subcells/' + subproj): |
| add_projects(projectpath + '/subcells/' + subproj) |
| |
| for item in os.listdir(self.projectdir): |
| if os.path.isdir(self.projectdir + '/' + item): |
| projectpath = self.projectdir + '/' + item |
| add_projects(projectpath) |
| |
| |
| # 'import' and others in the blacklist are not projects! |
| # Files beginning with '.' and files with whitespace are |
| # also not listed. |
| for item in projectlist[:]: |
| name = os.path.split(item)[1] |
| if self.blacklisted(name): |
| projectlist.remove(item) |
| elif badrex1.match(name): |
| projectlist.remove(item) |
| elif badrex2.match(name): |
| projectlist.remove(item) |
| 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 = '' |
| foundry_name = '' |
| node = '' |
| description = '' |
| status = 'active' |
| if pdkdir: |
| # Code should only be for efabless platform |
| ''' |
| 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 'foundry-name' in nodeinfo: |
| foundry_name = nodeinfo['foundry-name'] |
| if 'node' in nodeinfo: |
| node = nodeinfo['node'] |
| if 'description' in nodeinfo: |
| description = nodeinfo['description'] |
| if 'status' in nodeinfo: |
| status = nodeinfo['status'] |
| return foundry, foundry_name, 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 'foundry-name' in nodeinfo: |
| foundry_name = nodeinfo['foundry-name'] |
| if 'node' in nodeinfo: |
| node = nodeinfo['node'] |
| if 'description' in nodeinfo: |
| description = nodeinfo['description'] |
| if 'status' in nodeinfo: |
| status = nodeinfo['status'] |
| return foundry, foundry_name, node, description, status |
| raise Exception('malformed pdkPath: can\'t determine foundry or node') |
| |
| #------------------------------------------------------------------------ |
| # 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, foundry_name, 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 PDK directory for projects without a techdir (most likely the project is being imported) |
| #------------------------------------------------------------------------ |
| @classmethod |
| def get_import_pdk(cls, projectpath): |
| print(projectpath) |
| jsonname = projectpath + '/project.json' |
| |
| with open(jsonname, 'r') as f: |
| datatop = json.load(f) |
| project_data = datatop['project'] |
| project_foundry = project_data['foundry'] |
| project_process = project_data['process'] |
| |
| project_pdkdir = '' |
| |
| for pdkdir_lr in glob.glob('/usr/share/pdk/*/libs.tech/'): |
| pdkdir = os.path.split( os.path.split( pdkdir_lr )[0])[0] |
| foundry, foundry_name, node, desc, status = ProjectManager.pdkdir2fnd( pdkdir ) |
| if not foundry or not node: |
| continue |
| if (foundry == project_foundry or foundry_name == project_foundry) and node == project_process: |
| project_pdkdir = pdkdir |
| break |
| |
| return project_pdkdir, foundry, node #------------------------------------------------------------------------ |
| # 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 YAML 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. |
| |
| jaonfile = newproject + '/project.json' |
| |
| try: |
| shutil.copy(importfile, jsonfile) |
| except IOError as e: |
| print('Error copying files: ' + str(e)) |
| return None |
| |
| # (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 |
| |
| #------------------------------------------------------------------------ |
| # Create project.json file (automatically done in create_project.py in |
| # case it's executed from the command line) |
| #------------------------------------------------------------------------ |
| |
| def create_json(self, ipname, pdk_dir, description="(Add project description here)"): |
| # ipname: Project Name |
| data = {} |
| project= {} |
| project['description'] = description |
| try: |
| project['foundry'], foundry_name, project['process'], pdk_desc, pdk_stat = self.pdkdir2fnd( pdk_dir ) |
| except: |
| # Cannot parse PDK name, so foundry and node will remain undefined |
| pass |
| project['project_name'] = ipname |
| project['flow'] = 'none' |
| data['project']=project |
| return data |
| |
| #------------------------------------------------------------------------ |
| # 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 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 |
| # project.json file has a "stdcell" entry for the subproject, then |
| # add the line "techname=" with the name of the standard cell |
| # library as pulled from project.json. |
| |
| 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/project_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 |
| path = value['values'][0] |
| 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 |
| if os.path.islink(path): |
| os.unlink(path) |
| self.update_project_views() |
| else: |
| shutil.rmtree(value['values'][0]) |
| if ('subcells' in path): |
| self.update_project_views() |
| |
| #---------------------------------------------------------------------- |
| # 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 |
| else: |
| self.clean(ppath) |
| |
| def clean(self, ppath): |
| 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): |
| global currdesign |
| global apps_path |
| # 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'] |
| |
| # Find out whether the user wants to create a subproject or project |
| parent_pdk = '' |
| try: |
| with open(os.path.expanduser(currdesign), 'r') as f: |
| pdirCur = f.read().rstrip() |
| if ('subcells' in pdirCur): |
| # subproject is selected |
| parent_path = os.path.split(os.path.split(pdirCur)[0])[0] |
| pdkdir = self.get_pdk_dir(parent_path, path=True) |
| (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( pdkdir ) |
| parent_pdk = foundry + '/' + node |
| warning = 'Create new subproject in '+ parent_path + ':' |
| elif (pdirCur[0] == '.'): |
| # the project's 'subproject' of itself is selected |
| parent_path = pdirCur[1:] |
| pdkdir = self.get_pdk_dir(parent_path, path=True) |
| (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( pdkdir ) |
| parent_pdk = foundry + '/' + node |
| warning = 'Create new subproject in '+ parent_path + ':' |
| |
| except: |
| pass |
| |
| while True: |
| try: |
| if seedname: |
| newname, newpdk = NewProjectDialog(self, warning, seed=seedname, importnode=importnode, development=development, parent_pdk=parent_pdk).result |
| else: |
| newname, newpdk = NewProjectDialog(self, warning, seed='', importnode=importnode, development=development, parent_pdk=parent_pdk).result |
| except TypeError: |
| # TypeError occurs when "Cancel" is pressed, just handle exception. |
| return None |
| if not newname: |
| return None # Canceled, no action. |
| |
| if parent_pdk == '': |
| newproject = self.projectdir + '/' + newname |
| else: |
| newproject = parent_path + '/subcells/' + 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 |
| |
| if parent_pdk !='' and not os.path.isdir(parent_path + '/subcells'): |
| os.makedirs(parent_path + '/subcells') |
| |
| try: |
| subprocess.Popen([apps_path + '/create_project.py', newproject, newpdk]).wait() |
| |
| # Show subproject in project view |
| if parent_pdk != '': |
| self.update_project_views() |
| |
| 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/' + 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') |
| spinit = newpdk + '/libs.tech/ngpsice/spinit' |
| if os.path.exists(spinit): |
| shutil.copy(spinit, newproject + '/xschem/.spiceinit') |
| |
| 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: json file has multiple documents, so must use |
| # json.load_all(), not json.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 + '/.config/nodeinfo.json'): |
| print("Reading nodeinfo.json file:") |
| jdicts = [] |
| |
| with open(ppath + '/.config/nodeinfo.json', 'r') as ifile: |
| jsondata = json.load_all(ifile, Loader=json.Loader) |
| for jdict in jsondata: |
| jdicts.append(jdict) |
| |
| for jdict in jdicts: |
| for jentry in jdict.values(): |
| if 'process' in jentry: |
| importnode = jentry['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. |
| |
| jdicts = [] |
| 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 json.load_all(), not json.load() (see above) |
| |
| if os.path.exists(ppath + '/.config/nodeinfo.json'): |
| print("Reading YAML file:") |
| jdicts = [] |
| with open(ppath + '/.config/nodeinfo.json', 'r') as ifile: |
| jsondata = json.load_all(ifile, Loader=json.Loader) |
| for jdict in jsondata: |
| jdicts.append(jdict) |
| |
| for jdict in jdicts: |
| for jentry in jdict.values(): |
| if 'process' in jentry: |
| importnode = jentry['process'] |
| if 'stdcell' in jentry: |
| stdcell = jentry['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 newname == "": |
| warning = 'Please enter 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: |
| if os.path.islink(oldpath): |
| os.symlink(oldpath, newproject) |
| else: |
| 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 "project_name". Using |
| # the project filename as a project name is a fallback behavior. If |
| # there is a project.json file, and it defines a project_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 project_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' |
| |
| found = False |
| if os.path.isfile(jsonname): |
| # Pull the project_name into local store (may want to do this with the |
| # datasheet as well) |
| with open(jsonname, 'r') as f: |
| datatop = json.safe_load(f) |
| if 'project_name' in datatop['project']: |
| found = True |
| |
| if not found: |
| pdkdir = self.get_pdk_dir(newproject, path=True) |
| yData = self.create_json(oldname, pdkdir) |
| with open(newproject + '/project.json', 'w') as ofile: |
| json.dump(yData, ofile) |
| |
| # 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) |
| ''' |
| |
| #---------------------------------------------------------------------- |
| # Allow the user to choose the flow of the project |
| #---------------------------------------------------------------------- |
| |
| def startflow(self, value): |
| projectpath = value['values'][0] |
| flow = '' |
| warning = 'Select a flow for '+value['text'] |
| is_subproject = False |
| try: |
| with open(os.path.expanduser(currdesign), 'r') as f: |
| pdirCur = f.read().rstrip() |
| if ('subcells' in pdirCur): |
| # subproject is selected |
| is_subproject = True |
| except: |
| pass |
| if not os.path.exists(projectpath + '/project.json'): |
| project_pdkdir = self.get_pdk_dir(projectpath, path=True) |
| data = self.create_json(os.path.split(projectpath)[1], project_pdkdir) |
| with open(projectpath + '/project.json', 'w') as ofile: |
| json.dump(data, ofile) |
| |
| # Read json file for the selected flow |
| with open(projectpath + '/project.json','r') as f: |
| data = json.safe_load(f) |
| project = data['project'] |
| if 'flow' in project.keys() and project['flow']=='none' or 'flow' not in project.keys(): |
| while True: |
| try: |
| flow = SelectFlowDialog(self, warning, seed='', is_subproject = is_subproject).result |
| except TypeError: |
| # TypeError occurs when "Cancel" is pressed, just handle exception. |
| return None |
| if not flow: |
| return None # Canceled, no action. |
| break |
| project['flow']=flow |
| data['project']=project |
| with open(projectpath + '/project.json', 'w') as ofile: |
| json.dump(data, ofile) |
| else: |
| flow = project['flow'] |
| |
| print("Starting "+flow+" flow...") |
| if flow.lower() == 'digital': |
| self.synthesize() |
| |
| #---------------------------------------------------------------------- |
| # 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 YAML 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' |
| |
| 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.safe_load(f) |
| project_data = datatop['project'] |
| if 'project_name' in project_data: |
| oldname = project_data['project_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 a project or subproject to the project manager |
| #---------------------------------------------------------------------- |
| |
| def importproject(self, value): |
| warning = "Import project:" |
| badrex1 = re.compile("^\.") |
| badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*") |
| print(warning) |
| |
| # Find out whether the user wants to import a subproject or project based on what they selected in the treeview |
| parent_pdk = '' |
| parent_path = '' |
| try: |
| with open(os.path.expanduser(currdesign), 'r') as f: |
| pdirCur = f.read().rstrip() |
| if ('subcells' in pdirCur): |
| # subproject is selected |
| parent_path = os.path.split(os.path.split(pdirCur)[0])[0] |
| parent_pdkdir = self.get_pdk_dir(parent_path, path=True) |
| (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( parent_pdkdir ) |
| parent_pdk = foundry + '/' + node |
| warning = 'Import a subproject to '+ parent_path + ':' |
| elif (pdirCur[0] == '.'): |
| # the project's 'subproject' of itself is selected |
| parent_path = pdirCur[1:] |
| parent_pdkdir = self.get_pdk_dir(parent_path, path=True) |
| (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( parent_pdkdir ) |
| parent_pdk = foundry + '/' + node |
| warning = 'Import a subproject to '+ parent_path + ':' |
| |
| except: |
| pass |
| |
| while True: |
| try: |
| newname, project_pdkdir, projectpath, importoption = ImportDialog(self, warning, seed='', parent_pdk = parent_pdk, parent_path = parent_path, project_dir = self.projectdir).result |
| except TypeError: |
| # TypeError occurs when "Cancel" is pressed, just handle exception. |
| return None |
| if not newname: |
| return None # Canceled, no action. |
| |
| if parent_pdk == '': |
| newproject = self.projectdir + '/' + newname |
| else: |
| newproject = parent_path + '/subcells/' + newname |
| break |
| |
| def make_techdirs(projectpath, project_pdkdir): |
| # Recursively create techdirs in project and subproject folders |
| if not (os.path.exists(projectpath + '/.config') or os.path.exists(projectpath + '/.ef-config')): |
| os.makedirs(projectpath + '/.config') |
| if not os.path.exists(projectpath + self.config_path(projectpath) + '/techdir'): |
| os.symlink(project_pdkdir, projectpath + self.config_path(projectpath) + '/techdir') |
| if os.path.isdir(projectpath + '/subcells'): |
| for subproject in os.listdir(projectpath + '/subcells'): |
| subproject_path = projectpath + '/subcells/' + subproject |
| make_techdirs(subproject_path, project_pdkdir) |
| |
| make_techdirs(projectpath, project_pdkdir) |
| |
| # Make symbolic link/copy projects |
| if parent_path=='': |
| # Create a regular project |
| if importoption == "link": |
| os.symlink(projectpath, self.projectdir + '/' + newname) |
| else: |
| shutil.copytree(projectpath, self.projectdir + '/' + newname, symlinks = True) |
| if not os.path.exists(projectpath + '/project.json'): |
| jData = self.create_json(newname, project_pdkdir) |
| with open(projectpath + '/project.json', 'w') as ofile: |
| json.dump(jData, ofile) |
| else: |
| #Create a subproject |
| if not os.path.exists(parent_path + '/subcells'): |
| os.makedirs(parent_path + '/subcells') |
| if importoption == "copy": |
| shutil.copytree(projectpath, parent_path + '/subcells/' + newname, symlinks = True) |
| if parent_pdkdir != project_pdkdir: |
| self.clean(parent_path + '/subcells/' + newname) |
| else: |
| os.symlink(projectpath, parent_path + '/subcells/' + newname) |
| if not os.path.exists(parent_path + '/subcells/' + newname + '/project.json'): |
| yData = self.create_json(newname, project_pdkdir) |
| with open(parent_path + '/subcells/' + newname + '/project.json', 'w') as ofile: |
| json.dump(yData, ofile) |
| self.update_project_views() |
| #---------------------------------------------------------------------- |
| # "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): |
| global apps_path |
| 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([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] # project path |
| pdkdir = self.get_pdk_dir(design, path = True) |
| qflowdir = pdkdir + '/libs.tech/qflow' |
| # 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) |
| print('Loading digital flow manager...') |
| #TODO: replace hard-coded path with function that gets the qflow manager path |
| if development: |
| subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py', |
| qflowdir, design, '-development', '-subproject=' + pname]) |
| else: |
| subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py', |
| qflowdir, design, '-subproject=' + pname]) |
| else: |
| print('Synthesize design ' + designname + ' (' + design + ')') |
| print('Loading digital flow manager...') |
| # use Popen, not run, so that application does not wait for it to exit. |
| if development: |
| subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py', |
| qflowdir, design, designname, '-development']) |
| else: |
| subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py', |
| qflowdir, 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): |
| ''' |
| global apps_path |
| 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', |
| apps_path + '/cace_design_upload.py', |
| design, '-test']) |
| ''' |
| |
| #-------------------------------------------------------------------------- |
| |
| # Runs whenever a user selects a project |
| def setcurrent(self, value): |
| global currdesign |
| treeview = value.widget |
| selection = treeview.item(treeview.selection()) # dict with text, values, tags, etc. as keys |
| pname = selection['text'] |
| pdir = treeview.selection()[0] # iid of the selected project |
| #print("setcurrent returned value " + pname) |
| metapath = os.path.expanduser(currdesign) |
| if not os.path.exists(metapath): |
| os.makedirs(os.path.split(metapath)[0], exist_ok=True) |
| with open(metapath, 'w') as f: |
| f.write(pdir + '\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'] |
| #print("svalues :"+str(svalues)) |
| 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') |
| |
| # 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 "project_name" in the project.json file, which is separate from |
| # the datasheet. |
| |
| found = False |
| ppath = selection['values'][0] |
| jsonname = ppath + '/project.json' |
| |
| if os.path.isfile(jsonname): |
| # Pull the project_name into local store |
| with open(jsonname, 'r') as f: |
| datatop = json.safe_load(f) |
| project_data = datatop['project'] |
| ipname = project_data['project_name'] |
| self.project_name = ipname |
| else: |
| print('Setting project ip-name from the project folder name.') |
| self.project_name = pname |
| jsonname = ppath + '/project.json' |
| if os.path.isfile(jsonname): |
| with open(jsonname, 'r') as f: |
| datatop = json.load(f) |
| dsheet = datatop['data-sheet'] |
| found = True |
| else: |
| # Use 'pname' as the default project name. |
| print('No characterization file ' + jsonname) |
| |
| # 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 application |
| #---------------------------------------------------------------------------- |
| |
| if __name__ == '__main__': |
| faulthandler.register(signal.SIGUSR2) |
| |
| ProjectManager(root) |
| if deferLoad: |
| # Without this, mainloop may find and run very short clock-delayed |
| # events before the main form displays. With it, the first project |
| # load can be scheduled using after-time=0 |
| root.update_idletasks() |
| root.mainloop() |