blob: 921b3f1166d49d550c1867004b78fb9c01a26862 [file] [log] [blame]
emayecs5966a532021-07-29 10:07:02 -04001#!/ef/efabless/opengalaxy/venv/bin/python3 -B
2#
3#--------------------------------------------------------
4# Open Galaxy Project Manager GUI.
5#
6# This is a Python tkinter script that handles local
7# project management. It is meant as a replacement for
8# appsel_zenity.sh
9#
10#--------------------------------------------------------
11# Written by Tim Edwards
12# efabless, inc.
13# September 9, 2016
14# Modifications 2017, 2018
15# Version 1.0
16#--------------------------------------------------------
17
18import sys
19# Require python 3.5.x (and not python 3.6.x). Without this trap here, in several
20# instances of VMs where /usr/bin/python3 symlinked to 3.6.x by mistake, it manifests
21# as (misleading) errors like: ImportError: No module named 'yaml'
22#
23# '%x' % sys.hexversion -> '30502f0'
24
25import tkinter
26from tkinter import ttk, StringVar, Listbox, END
27from tkinter import filedialog
28
29# globals
30theProg = sys.argv[0]
31root = tkinter.Tk() # WARNING: must be exactly one instance of Tk; don't call again elsewhere
32
33# 4 configurations based on booleans: splash,defer
34# n,n: no splash, show only form when completed: LEGACY MODE, user confused by visual lag.
35# n,y: no splash but defer projLoad: show an empty form ASAP
36# y,n: yes splash, and wait for projLoad before showing completed form
37# y,y: yes splash, but also defer projLoad: show empty form ASAP
38
39# deferLoad = False # LEGACY: no splash, and wait for completed form
40# doSplash = False
41
42deferLoad = True # True: display GUI before (slow) loading of projects, so no splash:
43doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects
44
45# deferLoad = False # load projects before showing form, so need splash:
46# doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects
47
48# deferLoad = True # here keep splash also, despite also deferred-loading
49# doSplash = True
50
51#------------------------------------------------------
52# Splash screen: display ASAP: BEFORE bulk of imports.
53#------------------------------------------------------
54
55class SplashScreen(tkinter.Toplevel):
56 """Open Galaxy Project Management Splash Screen"""
57
58 def __init__(self, parent, *args, **kwargs):
59 super().__init__(parent, *args, **kwargs)
60 parent.withdraw()
61 #EFABLESS PLATFORM
62 #image = tkinter.PhotoImage(file="/ef/efabless/opengalaxy/og_splashscreen50.gif")
63 label = ttk.Label(self, image=image)
64 label.pack()
65
66 # required to make window show before the program gets to the mainloop
67 self.update_idletasks()
68
69import faulthandler
70import signal
71
72# SplashScreen here. fyi: there's a 2nd/later __main__ section for main app
73splash = None # a global
74if __name__ == '__main__':
75 faulthandler.register(signal.SIGUSR2)
76 if doSplash:
77 splash = SplashScreen(root)
78
79import io
80import os
81import re
82import json
83import yaml
84import shutil
85import tarfile
86import datetime
87import subprocess
88import contextlib
89import tempfile
90import glob
91
92import tksimpledialog
93import tooltip
94from rename_project import rename_project_all
95#from fix_libdirs import fix_libdirs
96from consoletext import ConsoleText
97from helpwindow import HelpWindow
98from treeviewchoice import TreeViewChoice
99from symbolbuilder import SymbolBuilder
100from make_icon_from_soft import create_symbol
101from profile import Profile
102
103import og_config
104
105# Global name for design directory
106designdir = 'design'
107# Global name for import directory
108importdir = 'import'
109# Global name for cloudv directory
110cloudvdir = 'cloudv'
111# Global name for archived imports project sub-directory
112archiveimportdir = 'imported'
113# Global name for current design file
114#EFABLESS PLATFORM
115currdesign = '~/.open_pdks/currdesign'
116prefsfile = '~/.open_pdks/prefs.json'
117
118
119#---------------------------------------------------------------
120# Watch a directory for modified time change. Repeat every two
121# seconds. Call routine callback() if a change occurs
122#---------------------------------------------------------------
123
124class WatchClock(object):
125 def __init__(self, parent, path, callback, interval=2000, interval0=None):
126 self.parent = parent
127 self.callback = callback
128 self.path = path
129 self.interval = interval
130 if interval0 != None:
131 self.interval0 = interval0
132 self.restart(first=True)
133 else:
134 self.interval0 = interval
135 self.restart()
136
137 def query(self):
138 for entry in self.path:
139 statbuf = os.stat(entry)
140 if statbuf.st_mtime > self.reftime:
141 self.callback()
142 self.restart()
143 return
144 self.timer = self.parent.after(self.interval, self.query)
145
146 def stop(self):
147 self.parent.after_cancel(self.timer)
148
149 # if first: optionally use different (typically shorter) interval, AND DON'T
150 # pre-record watched-dir mtime-s (which forces the callback on first timer fire)
151 def restart(self, first=False):
152 self.reftime = 0
153 if not first:
154 for entry in self.path:
155 statbuf = os.stat(entry)
156 if statbuf.st_mtime > self.reftime:
157 self.reftime = statbuf.st_mtime
158 self.timer = self.parent.after(self.interval0 if first and self.interval0 != None else self.interval, self.query)
159
160#------------------------------------------------------
161# Dialog for generating a new layout
162#------------------------------------------------------
163
164class NewLayoutDialog(tksimpledialog.Dialog):
165 def body(self, master, warning, seed=''):
166 if warning:
167 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
168
169 self.l1prefs = tkinter.IntVar(master)
170 self.l1prefs.set(1)
171 ttk.Checkbutton(master, text='Populate new layout from netlist',
172 variable = self.l1prefs).grid(row = 2, columnspan = 2, sticky = 'enws')
173
174 return self
175
176 def apply(self):
177 return self.l1prefs.get
178
179#------------------------------------------------------
180# Simple dialog for entering project names
181#------------------------------------------------------
182
183class ProjectNameDialog(tksimpledialog.Dialog):
184 def body(self, master, warning, seed=''):
185 if warning:
186 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
187 ttk.Label(master, text='Enter new project name:').grid(row = 1, column = 0, sticky = 'wns')
188 self.nentry = ttk.Entry(master)
189 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
190 self.nentry.insert(0, seed)
191 return self.nentry # initial focus
192
193 def apply(self):
194 return self.nentry.get()
195
196class PadFrameCellNameDialog(tksimpledialog.Dialog):
197 def body(self, master, warning, seed=''):
198 description='PadFrame' # TODO: make this an extra optional parameter of a generic CellNameDialog?
199 if warning:
200 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
201 if description:
202 description = description + " "
203 else:
204 description = ""
205 ttk.Label(master, text=("Enter %scell name:" %(description))).grid(row = 1, column = 0, sticky = 'wns')
206 self.nentry = ttk.Entry(master)
207 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
208 self.nentry.insert(0, seed)
209 return self.nentry # initial focus
210
211 def apply(self):
212 return self.nentry.get()
213
214#------------------------------------------------------
215# Dialog for copying projects. Includes checkbox
216# entries for preferences.
217#------------------------------------------------------
218
219class CopyProjectDialog(tksimpledialog.Dialog):
220 def body(self, master, warning, seed=''):
221 if warning:
222 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
223 ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0, sticky = 'wns')
224 self.nentry = ttk.Entry(master)
225 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
226 self.nentry.insert(0, seed)
227 self.elprefs = tkinter.IntVar(master)
228 self.elprefs.set(0)
229 ttk.Checkbutton(master, text='Copy electric preferences (not recommended)',
230 variable = self.elprefs).grid(row = 2, columnspan = 2, sticky = 'enws')
231 self.spprefs = tkinter.IntVar(master)
232 self.spprefs.set(0)
233 ttk.Checkbutton(master, text='Copy ngspice folder (not recommended)',
234 variable = self.spprefs).grid(row = 3, columnspan = 2, sticky = 'enws')
235 return self.nentry # initial focus
236
237 def apply(self):
238 # Return a list containing the entry text and the checkbox states.
239 elprefs = True if self.elprefs.get() == 1 else False
240 spprefs = True if self.spprefs.get() == 1 else False
241 return [self.nentry.get(), elprefs, spprefs]
242
243#-------------------------------------------------------
244# Not-Quite-So-Simple dialog for entering a new project.
245# Select a project name and a PDK from a drop-down list.
246#-------------------------------------------------------
247
248class NewProjectDialog(tksimpledialog.Dialog):
249 def body(self, master, warning, seed='', importnode=None, development=False):
250 if warning:
251 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
252 ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0)
253 self.nentry = ttk.Entry(master)
254 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
255 self.nentry.insert(0, seed or '') # may be None
256 self.pvar = tkinter.StringVar(master)
257 if not importnode:
258 # Add PDKs as found by searching /ef/tech for 'libs.tech' directories
259 ttk.Label(master, text="Select foundry/node:").grid(row = 2, column = 0)
260 else:
261 ttk.Label(master, text="Foundry/node:").grid(row = 2, column = 0)
262 self.infolabel = ttk.Label(master, text="", style = 'brown.TLabel', wraplength=250)
263 self.infolabel.grid(row = 3, column = 0, columnspan = 2, sticky = 'news')
264 self.pdkmap = {}
265 self.pdkdesc = {}
266 self.pdkstat = {}
267 pdk_def = None
268
269 node_def = importnode
270 if not node_def:
271 node_def = "EFXH035B"
272
273 # use glob instead of os.walk. Don't need to recurse large PDK hier.
274 # TODO: stop hardwired default EFXH035B: get from an overall flow /ef/tech/.ef-config/plist.json
275 # (or get it from the currently selected project)
276 #EFABLESS PLATFORM
277 #TODO: Replace with PREFIX
278 for pdkdir_lr in glob.glob('/usr/share/pdk/*/libs.tech/'):
279 pdkdir = os.path.split( os.path.split( pdkdir_lr )[0])[0] # discard final .../libs.tech/
280 (foundry, node, desc, status) = OpenGalaxyManager.pdkdir2fnd( pdkdir )
281 if not foundry or not node:
282 continue
283 key = foundry + '/' + node
284 self.pdkmap[key] = pdkdir
285 self.pdkdesc[key] = desc
286 self.pdkstat[key] = status
287 if node == node_def and not pdk_def:
288 pdk_def = key
289
290 # Quick hack: sorting puts EFXH035A before EFXH035LEGACY. However, some
291 # ranking is needed.
292 pdklist = sorted( self.pdkmap.keys())
293 if not pdklist:
294 raise ValueError( "assertion failed, no available PDKs found")
295 pdk_def = (pdk_def or pdklist[0])
296
297 self.pvar.set(pdk_def)
298
299 # Restrict list to single entry if importnode was non-NULL and
300 # is in the PDK list (OptionMenu is replaced by a simple label)
301 # Otherwise, restrict the list to entries having an "status"
302 # entry equal to "active". This allows some legacy PDKs to be
303 # disabled for creating new projects (but available for projects
304 # that already have them).
305
306 if importnode:
307 self.pdkselect = ttk.Label(master, text = pdk_def, style='blue.TLabel')
308 else:
309 pdkactive = list(item for item in pdklist if self.pdkstat[item] == 'active')
310 if development:
311 pdkactive.extend(list(item for item in pdklist if self.pdkstat[item] == 'development'))
312
313 self.pdkselect = ttk.OptionMenu(master, self.pvar, pdk_def, *pdkactive,
314 style='blue.TMenubutton', command=self.show_info)
315 self.pdkselect.grid(row = 2, column = 1)
316 self.show_info(0)
317
318 return self.nentry # initial focus
319
320 def show_info(self, args):
321 key = str(self.pvar.get())
322 desc = self.pdkdesc[key]
323 if desc == '':
324 self.infolabel.config(text='(no description available)')
325 else:
326 self.infolabel.config(text=desc)
327
328 def apply(self):
329 return self.nentry.get(), self.pdkmap[ str(self.pvar.get()) ] # Note converts StringVar to string
330
331#----------------------------------------------------------------
332# Not-Quite-So-Simple dialog for selecting an existing project.
333# Select a project name from a drop-down list. This could be
334# replaced by simply using the selected (current) project.
335#----------------------------------------------------------------
336
337class ExistingProjectDialog(tksimpledialog.Dialog):
338 def body(self, master, plist, seed, warning='Enter name of existing project to import into:'):
339 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
340
341 # Alphebetize list
342 plist.sort()
343 # Add projects
344 self.pvar = tkinter.StringVar(master)
345 self.pvar.set(plist[0])
346
347 ttk.Label(master, text='Select project:').grid(row = 1, column = 0)
348
349 self.projectselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton')
350 self.projectselect.grid(row = 1, column = 1, sticky = 'ewns')
351 # pack version (below) hangs. Don't know why, changed to grid (like ProjectNameDialog)
352 # self.projectselect.pack(side = 'top', fill = 'both', expand = 'true')
353 return self.projectselect # initial focus
354
355 def apply(self):
356 return self.pvar.get() # Note converts StringVar to string
357
358#----------------------------------------------------------------
359# Not-Quite-So-Simple dialog for selecting an existing ElecLib of existing project.
360# Select an elecLib name from a drop-down list.
361#----------------------------------------------------------------
362
363class ExistingElecLibDialog(tksimpledialog.Dialog):
364 def body(self, master, plist, seed):
365 warning = "Enter name of existing Electric library to import into:"
366 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
367
368 # Alphebetize list
369 plist.sort()
370 # Add electric libraries
371 self.pvar = tkinter.StringVar(master)
372 self.pvar.set(plist[0])
373
374 ttk.Label(master, text="Select library:").grid(row = 1, column = 0)
375
376 self.libselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton')
377 self.libselect.grid(row = 1, column = 1)
378 return self.libselect # initial focus
379
380 def apply(self):
381 return self.pvar.get() # Note converts StringVar to string
382
383#----------------------------------------------------------------
384# Dialog for layout, in case of multiple layout names, none of
385# which matches the project name (ip-name). Method: Select a
386# layout name from a drop-down list. If there is no project.json
387# file, add a checkbox for creating one and seeding the ip-name
388# with the name of the selected layout. Include entry for
389# new layout, and for new layouts add a checkbox to import the
390# layout from schematic or verilog, if a valid candidate exists.
391#----------------------------------------------------------------
392
393class EditLayoutDialog(tksimpledialog.Dialog):
394 def body(self, master, plist, seed='', ppath='', pname='', warning='', hasnet=False):
395 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
396 self.ppath = ppath
397 self.pname = pname
398
399 # Checkbox variable
400 self.confirm = tkinter.IntVar(master)
401 self.confirm.set(0)
402
403 # To-Do: Add checkbox for netlist import
404
405 # Alphebetize list
406 plist.sort()
407 # Add additional item for new layout
408 plist.append('(New layout)')
409
410 # Add layouts to list
411 self.pvar = tkinter.StringVar(master)
412 self.pvar.set(plist[0])
413
414 ttk.Label(master, text='Selected layout to edit:').grid(row = 1, column = 0)
415
416 if pname in plist:
417 pseed = plist.index(pname)
418 else:
419 pseed = 0
420
421 self.layoutselect = ttk.OptionMenu(master, self.pvar, plist[pseed], *plist,
422 style='blue.TMenubutton', command=self.handle_choice)
423 self.layoutselect.grid(row = 1, column = 1, sticky = 'ewns')
424
425 # Create an entry form and checkbox for entering a new layout name, but
426 # keep them unpacked unless the "(New layout)" selection is chosen.
427
428 self.layoutbox = ttk.Frame(master)
429 self.layoutlabel = ttk.Label(self.layoutbox, text='New layout name:')
430 self.layoutlabel.grid(row = 0, column = 0, sticky = 'ewns')
431 self.layoutentry = ttk.Entry(self.layoutbox)
432 self.layoutentry.grid(row = 0, column = 1, sticky = 'ewns')
433 self.layoutentry.insert(0, pname)
434
435 # Only allow 'makeproject' checkbox if there is no project.json file
436 jname = ppath + '/project.json'
437 if not os.path.exists(jname):
438 dname = os.path.split(ppath)[1]
439 jname = ppath + '/' + dname + '.json'
440 if not os.path.exists(jname):
441 self.makeproject = ttk.Checkbutton(self.layoutbox,
442 text='Make default project name',
443 variable = self.confirm)
444 self.makeproject.grid(row = 2, column = 0, columnspan = 2, sticky = 'ewns')
445 return self.layoutselect # initial focus
446
447 def handle_choice(self, event):
448 if self.pvar.get() == '(New layout)':
449 # Add entry and checkbox for creating ad-hoc project.json file
450 self.layoutbox.grid(row = 1, column = 0, columnspan = 2, sticky = 'ewns')
451 else:
452 # Remove entry and checkbox
453 self.layoutbox.grid_forget()
454 return
455
456 def apply(self):
457 if self.pvar.get() == '(New layout)':
458 if self.confirm.get() == 1:
459 pname = self.pname
460 master.create_ad_hoc_json(self.layoutentry.get(), pname)
461 return self.layoutentry.get()
462 else:
463 return self.pvar.get() # Note converts StringVar to string
464
465#----------------------------------------------------------------
466# Dialog for padframe: select existing ElecLib of existing project, type in a cellName.
467# Select an elecLib name from a drop-down list.
468# Text field for entry of a cellName.
469#----------------------------------------------------------------
470
471class ExistingElecLibCellDialog(tksimpledialog.Dialog):
472 def body(self, master, descPre, seed='', descPost='', plist=None, seedLibNm=None, seedCellNm=''):
473 warning = 'Pick existing Electric library; enter cell name'
474 warning = (descPre or '') + ((descPre and ': ') or '') + warning + ((descPost and ' ') or '') + (descPost or '')
475 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
476
477 # Alphebetize list
478 plist.sort()
479 # Add electric libraries
480 self.pvar = tkinter.StringVar(master)
481 pNdx = 0
482 if seedLibNm and seedLibNm in plist:
483 pNdx = plist.index(seedLibNm)
484 self.pvar.set(plist[pNdx])
485
486 ttk.Label(master, text='Electric library:').grid(row = 1, column = 0, sticky = 'ens')
487 self.libselect = ttk.OptionMenu(master, self.pvar, plist[pNdx], *plist, style='blue.TMenubutton')
488 self.libselect.grid(row = 1, column = 1, sticky = 'wns')
489
490 ttk.Label(master, text=('cell name:')).grid(row = 2, column = 0, sticky = 'ens')
491 self.nentry = ttk.Entry(master)
492 self.nentry.grid(row = 2, column = 1, sticky = 'ewns')
493 self.nentry.insert(0, seedCellNm)
494
495 return self.libselect # initial focus
496
497 def apply(self):
498 # return list of 2 strings: selected ElecLibName, typed-in cellName.
499 return [self.pvar.get(), self.nentry.get()] # Note converts StringVar to string
500
501#------------------------------------------------------
502# Simple dialog for confirming anything.
503#------------------------------------------------------
504
505class ConfirmDialog(tksimpledialog.Dialog):
506 def body(self, master, warning, seed):
507 if warning:
508 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
509 return self
510
511 def apply(self):
512 return 'okay'
513
514#------------------------------------------------------
515# More proactive dialog for confirming an invasive
516# procedure like "delete project". Requires user to
517# click a checkbox to ensure this is not a mistake.
518# confirmPrompt can be overridden, default='I am sure I want to do this.'
519#------------------------------------------------------
520
521class ProtectedConfirmDialog(tksimpledialog.Dialog):
522 def body(self, master, warning, seed='', confirmPrompt=None):
523 if warning:
524 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
525 self.confirm = tkinter.IntVar(master)
526 self.confirm.set(0)
527 if not confirmPrompt:
528 confirmPrompt='I am sure I want to do this.'
529 ttk.Checkbutton(master, text=confirmPrompt,
530 variable = self.confirm).grid(row = 1, columnspan = 2, sticky = 'enws')
531 return self
532
533 def apply(self):
534 return 'okay' if self.confirm.get() == 1 else ''
535
536#------------------------------------------------------
537# Simple dialog to say "blah is not implemented yet."
538#------------------------------------------------------
539
540class NotImplementedDialog(tksimpledialog.Dialog):
541 def body(self, master, warning, seed):
542 if not warning:
543 warning = "Sorry, that feature is not implemented yet"
544 if warning:
545 warning = "Sorry, " + warning + ", is not implemented yet"
546 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
547 return self
548
549 def apply(self):
550 return 'okay'
551
552#------------------------------------------------------
553# (This is actually a generic confirm dialogue, no install/overwrite intelligence)
554# But so far dedicated to confirming the installation of one or more files,
555# with notification of which (if any) will overwrite existing files.
556#
557# The warning parameter is fully constructed by caller, as multiple lines as either:
558# For the import of module 'blah',
559# CONFIRM installation of (*: OVERWRITE existing):
560# * path1
561# path2
562# ....
563# or:
564# For the import of module 'blah',
565# CONFIRM installation of:
566# path1
567# path2
568# ....
569# TODO: bastardizes warning parameter as multiple lines. Implement some other way?
570#------------------------------------------------------
571
572class ConfirmInstallDialog(tksimpledialog.Dialog):
573 def body(self, master, warning, seed):
574 if warning:
575 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
576 return self
577
578 def apply(self):
579 return 'okay'
580
581#------------------------------------------------------
582# Open Galaxy Manager class
583#------------------------------------------------------
584
585class OpenGalaxyManager(ttk.Frame):
586 """Open Galaxy Project Management GUI."""
587
588 def __init__(self, parent, *args, **kwargs):
589 super().__init__(parent, *args, **kwargs)
590 self.root = parent
591 parent.withdraw()
592 # self.update()
593 self.update_idletasks() # erase small initial frame asap
594 self.init_gui()
595 parent.protocol("WM_DELETE_WINDOW", self.on_quit)
596 if splash:
597 splash.destroy()
598 parent.deiconify()
599
600 def on_quit(self):
601 """Exits program."""
602 quit()
603
604 def init_gui(self):
605 """Builds GUI."""
606 global designdir
607 global importdir
608 global archiveimportdir
609 global currdesign
610 global theProg
611 global deferLoad
612
613 message = []
614 allPaneOpen = False
615 prjPaneMinh = 10
616 iplPaneMinh = 4
617 impPaneMinh = 4
618
619 # if deferLoad: # temp. for testing... open all panes
620 # allPaneOpen = True
621
622 # Read user preferences
623 self.prefs = {}
624 self.read_prefs()
625
626 # Get default font size from user preferences
627 fontsize = self.prefs['fontsize']
628
629 s = ttk.Style()
630 available_themes = s.theme_names()
631 # print("themes: " + str(available_themes))
632 s.theme_use(available_themes[0])
633
634 s.configure('gray.TFrame', background='gray40')
635 s.configure('blue_white.TFrame', bordercolor = 'blue', borderwidth = 3)
636 s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'))
637 s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
638 foreground = 'brown', anchor = 'center')
639 s.configure('title2.TLabel', font=('Helvetica', fontsize, 'bold italic'),
640 foreground = 'blue')
641 s.configure('normal.TLabel', font=('Helvetica', fontsize))
642 s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
643 s.configure('brown.TLabel', font=('Helvetica', fontsize), foreground = 'brown3', background = 'gray95')
644 s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3')
645 s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
646 s.configure('normal.TButton', font=('Helvetica', fontsize), border = 3, relief = 'raised')
647 s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red', border = 3,
648 relief = 'raised')
649 s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3', border = 3,
650 relief = 'raised')
651 s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue', border = 3,
652 relief = 'raised')
653
654 # Create the help window
655 self.help = HelpWindow(self, fontsize=fontsize)
656
657 with io.StringIO() as buf, contextlib.redirect_stdout(buf):
658 self.help.add_pages_from_file(og_config.apps_path + '/manager_help.txt')
659 message = buf.getvalue()
660
661
662 # Set the help display to the first page
663 self.help.page(0)
664
665 # Create the profile settings window
666 self.profile = Profile(self, fontsize=fontsize)
667
668 # Variables used by option menus
669 self.seltype = tkinter.StringVar(self)
670 self.cur_project = tkinter.StringVar(self)
671 self.cur_import = "(nothing selected)"
672 self.project_name = ""
673
674 # Root window title
675 self.root.title('Open Galaxy Project Manager')
676 self.root.option_add('*tearOff', 'FALSE')
677 self.pack(side = 'top', fill = 'both', expand = 'true')
678
679 pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6)
680 pane.pack(side = 'top', fill = 'both', expand = 'true')
681 self.toppane = ttk.Frame(pane)
682 self.botpane = ttk.Frame(pane)
683
684 # All interior windows size to toppane
685 self.toppane.columnconfigure(0, weight = 1)
686 # Projects window resizes preferably to others
687 self.toppane.rowconfigure(3, weight = 1)
688
689 # Get username, and from it determine the project directory.
690 # Save this path, because it gets used often.
691 username = self.prefs['username']
692 self.projectdir = os.path.expanduser('~/' + designdir)
693 self.cloudvdir = os.path.expanduser('~/' + cloudvdir)
694
695 # Check that the project directory exists, and create it if not
696 if not os.path.isdir(self.projectdir):
697 os.makedirs(self.projectdir)
698
699 # Label with the user
700 self.toppane.user_frame = ttk.Frame(self.toppane)
701 self.toppane.user_frame.grid(row = 0, sticky = 'news')
702
703 # Put logo image in corner. Ignore if something goes wrong, as this
704 # is only decorative. Note: ef_logo must be kept as a record in self,
705 # or else it gets garbage collected.
706 try:
707 #EFABLESS PLATFORM
708 self.ef_logo = tkinter.PhotoImage(file='/ef/efabless/opengalaxy/efabless_logo_small.gif')
709 self.toppane.user_frame.logo = ttk.Label(self.toppane.user_frame, image=self.ef_logo)
710 self.toppane.user_frame.logo.pack(side = 'left', padx = 5)
711 except:
712 pass
713
714 self.toppane.user_frame.title = ttk.Label(self.toppane.user_frame, text='User:', style='red.TLabel')
715 self.toppane.user_frame.user = ttk.Label(self.toppane.user_frame, text=username, style='blue.TLabel')
716
717 self.toppane.user_frame.title.pack(side = 'left', padx = 5)
718 self.toppane.user_frame.user.pack(side = 'left', padx = 5)
719
720 #---------------------------------------------
721 ttk.Separator(self.toppane, orient='horizontal').grid(row = 1, sticky = 'news')
722 #---------------------------------------------
723
724 # List of projects:
725 self.toppane.design_frame = ttk.Frame(self.toppane)
726 self.toppane.design_frame.grid(row = 2, sticky = 'news')
727
728 self.toppane.design_frame.design_header = ttk.Label(self.toppane.design_frame, text='Projects',
729 style='title.TLabel')
730 self.toppane.design_frame.design_header.pack(side = 'left', padx = 5)
731
732 self.toppane.design_frame.design_header2 = ttk.Label(self.toppane.design_frame,
733 text='(' + self.projectdir + '/)', style='normal.TLabel')
734 self.toppane.design_frame.design_header2.pack(side = 'left', padx = 5)
735
736 # Get current project from ~/.efmeta/currdesign and set the selection.
737 try:
738 with open(os.path.expanduser(currdesign), 'r') as f:
739 pnameCur = f.read().rstrip()
740 except:
741 pnameCur = None
742
743 # Create listbox of projects
744 projectlist = self.get_project_list() if not deferLoad else []
745 height = min(10, max(prjPaneMinh, 2 + len(projectlist)))
746 self.projectselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, selectVal=pnameCur, natSort=True)
747 self.projectselect.populate("Available Projects:", projectlist,
748 [["Create", True, self.createproject],
749 ["Copy", False, self.copyproject],
750 ["Rename IP", False, self.renameproject],
751 ["<CloudV", True, self.cloudvimport],
752 ["Clean", False, self.cleanproject],
753 ["Delete", False, self.deleteproject]],
754 height=height, columns=[0, 1])
755 self.projectselect.grid(row = 3, sticky = 'news')
756 self.projectselect.bindselect(self.setcurrent)
757
758 tooltip.ToolTip(self.projectselect.get_button(0), text="Create a new project")
759 tooltip.ToolTip(self.projectselect.get_button(1), text="Make a copy of an entire project")
760 tooltip.ToolTip(self.projectselect.get_button(2), text="Rename a project folder")
761 tooltip.ToolTip(self.projectselect.get_button(3), text="Import CloudV project as new project")
762 tooltip.ToolTip(self.projectselect.get_button(4), text="Clean simulation data from project")
763 tooltip.ToolTip(self.projectselect.get_button(5), text="Delete an entire project")
764
765 pdklist = self.get_pdk_list(projectlist)
766 self.projectselect.populate2("PDK", projectlist, pdklist)
767
768 if pnameCur:
769 try:
770 curitem = next(item for item in projectlist if pnameCur == os.path.split(item)[1])
771 except StopIteration:
772 pass
773 else:
774 if curitem:
775 self.projectselect.setselect(pnameCur)
776
777 # Check that the import directory exists, and create it if not
778 if not os.path.isdir(self.projectdir + '/' + importdir):
779 os.makedirs(self.projectdir + '/' + importdir)
780
781 # Create a watchdog on the project and import directories
782 watchlist = [self.projectdir, self.projectdir + '/' + importdir]
783 if os.path.isdir(self.projectdir + '/upload'):
784 watchlist.append(self.projectdir + '/upload')
785
786 # Check the creation time of the project manager app itself. Because the project
787 # manager tends to be left running indefinitely, it is important to know when it
788 # has been updated. This is checked once every hour since it is really expected
789 # only to happen occasionally.
790
791 thisapp = [theProg]
792 self.watchself = WatchClock(self, thisapp, self.update_alert, 3600000)
793
794 #---------------------------------------------
795
796 # Add second button bar for major project applications
797 self.toppane.apptitle = ttk.Label(self.toppane, text='Tools:', style='title2.TLabel')
798 self.toppane.apptitle.grid(row = 4, sticky = 'news')
799 self.toppane.appbar = ttk.Frame(self.toppane)
800 self.toppane.appbar.grid(row = 5, sticky = 'news')
801
802 # Define the application buttons and actions
803 self.toppane.appbar.schem_button = ttk.Button(self.toppane.appbar, text='Edit Schematic',
804 command=self.edit_schematic, style = 'normal.TButton')
805 self.toppane.appbar.schem_button.pack(side = 'left', padx = 5)
806 self.toppane.appbar.layout_button = ttk.Button(self.toppane.appbar, text='Edit Layout',
807 command=self.edit_layout, style = 'normal.TButton')
808 self.toppane.appbar.layout_button.pack(side = 'left', padx = 5)
809 self.toppane.appbar.lvs_button = ttk.Button(self.toppane.appbar, text='Run LVS',
810 command=self.run_lvs, style = 'normal.TButton')
811 self.toppane.appbar.lvs_button.pack(side = 'left', padx = 5)
812 self.toppane.appbar.char_button = ttk.Button(self.toppane.appbar, text='Characterize',
813 command=self.characterize, style = 'normal.TButton')
814 self.toppane.appbar.char_button.pack(side = 'left', padx = 5)
815 self.toppane.appbar.synth_button = ttk.Button(self.toppane.appbar, text='Synthesis Flow',
816 command=self.synthesize, style = 'normal.TButton')
817 self.toppane.appbar.synth_button.pack(side = 'left', padx = 5)
818
819 self.toppane.appbar.padframeCalc_button = ttk.Button(self.toppane.appbar, text='Pad Frame',
820 command=self.padframe_calc, style = 'normal.TButton')
821 self.toppane.appbar.padframeCalc_button.pack(side = 'left', padx = 5)
822 '''
823 if self.prefs['schemeditor'] == 'xcircuit':
824 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor")
825 elif self.prefs['schemeditor'] == 'xschem':
826 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor")
827 else:
828 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor")
829
830 if self.prefs['layouteditor'] == 'klayout':
831 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor")
832 else:
833 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor")
834 '''
835 self.refreshToolTips()
836
837 tooltip.ToolTip(self.toppane.appbar.lvs_button, text="Start LVS tool")
838 tooltip.ToolTip(self.toppane.appbar.char_button, text="Start Characterization tool")
839 tooltip.ToolTip(self.toppane.appbar.synth_button, text="Start Digital Synthesis tool")
840 tooltip.ToolTip(self.toppane.appbar.padframeCalc_button, text="Start Pad Frame Generator")
841
842 #---------------------------------------------
843 ttk.Separator(self.toppane, orient='horizontal').grid(row = 6, sticky = 'news')
844 #---------------------------------------------
845 # List of IP libraries:
846 self.toppane.library_frame = ttk.Frame(self.toppane)
847 self.toppane.library_frame.grid(row = 7, sticky = 'news')
848
849 self.toppane.library_frame.library_header = ttk.Label(self.toppane.library_frame, text='IP Library:',
850 style='title.TLabel')
851 self.toppane.library_frame.library_header.pack(side = 'left', padx = 5)
852
853 self.toppane.library_frame.library_header2 = ttk.Label(self.toppane.library_frame,
854 text='(' + self.projectdir + '/ip/)', style='normal.TLabel')
855 self.toppane.library_frame.library_header2.pack(side = 'left', padx = 5)
856
857 self.toppane.library_frame.library_header3 = ttk.Button(self.toppane.library_frame,
858 text=(allPaneOpen and '-' or '+'), command=self.library_toggle, style = 'normal.TButton', width = 2)
859 self.toppane.library_frame.library_header3.pack(side = 'right', padx = 5)
860
861 # Create listbox of IP libraries
862 iplist = self.get_library_list() if not deferLoad else []
863 height = min(8, max(iplPaneMinh, 2 + len(iplist)))
864 self.ipselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, natSort=True)
865 self.ipselect.populate("IP Library:", iplist,
866 [], height=height, columns=[0, 1], versioning=True)
867 valuelist = self.ipselect.getvaluelist()
868 datelist = self.get_date_list(valuelist)
869 itemlist = self.ipselect.getlist()
870 self.ipselect.populate2("date", itemlist, datelist)
871 if allPaneOpen:
872 self.library_open()
873
874 #---------------------------------------------
875 ttk.Separator(self.toppane, orient='horizontal').grid(row = 9, sticky = 'news')
876 #---------------------------------------------
877 # List of imports:
878 self.toppane.import_frame = ttk.Frame(self.toppane)
879 self.toppane.import_frame.grid(row = 10, sticky = 'news')
880
881 self.toppane.import_frame.import_header = ttk.Label(self.toppane.import_frame, text='Imports:',
882 style='title.TLabel')
883 self.toppane.import_frame.import_header.pack(side = 'left', padx = 5)
884
885 self.toppane.import_frame.import_header2 = ttk.Label(self.toppane.import_frame,
886 text='(' + self.projectdir + '/import/)', style='normal.TLabel')
887 self.toppane.import_frame.import_header2.pack(side = 'left', padx = 5)
888
889 self.toppane.import_frame.import_header3 = ttk.Button(self.toppane.import_frame,
890 text=(allPaneOpen and '-' or '+'), command=self.import_toggle, style = 'normal.TButton', width = 2)
891 self.toppane.import_frame.import_header3.pack(side = 'right', padx = 5)
892
893 # Create listbox of imports
894 importlist = self.get_import_list() if not deferLoad else []
895 self.number_of_imports = len(importlist) if not deferLoad else None
896 height = min(8, max(impPaneMinh, 2 + len(importlist)))
897 self.importselect = TreeViewChoice(self.toppane, fontsize=fontsize, markDir=True, deferLoad=deferLoad)
898 self.importselect.populate("Pending Imports:", importlist,
899 [["Import As", False, self.importdesign],
900 ["Import Into", False, self.importintodesign],
901 ["Delete", False, self.deleteimport]], height=height, columns=[0, 1])
902 valuelist = self.importselect.getvaluelist()
903 datelist = self.get_date_list(valuelist)
904 itemlist = self.importselect.getlist()
905 self.importselect.populate2("date", itemlist, datelist)
906
907 tooltip.ToolTip(self.importselect.get_button(0), text="Import as a new project")
908 tooltip.ToolTip(self.importselect.get_button(1), text="Import into an existing project")
909 tooltip.ToolTip(self.importselect.get_button(2), text="Remove the import file(s)")
910 if allPaneOpen:
911 self.import_open()
912
913 #---------------------------------------------
914 # ttk.Separator(self, orient='horizontal').grid(column = 0, row = 8, columnspan=4, sticky='ew')
915 #---------------------------------------------
916
917 # Add a text window below the import to capture output. Redirect
918 # print statements to it.
919 self.botpane.console = ttk.Frame(self.botpane)
920 self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true')
921
922 self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4)
923 self.text_box.pack(side='left', fill='both', expand = 'true')
924 console_scrollbar = ttk.Scrollbar(self.botpane.console)
925 console_scrollbar.pack(side='right', fill='y')
926 # attach console to scrollbar
927 self.text_box.config(yscrollcommand = console_scrollbar.set)
928 console_scrollbar.config(command = self.text_box.yview)
929
930 # Give all the expansion weight to the message window.
931 # self.rowconfigure(9, weight = 1)
932 # self.columnconfigure(0, weight = 1)
933
934 # at bottom (legacy mode): window height grows by one row.
935 # at top the buttons share a row with user name, reduce window height, save screen real estate.
936 bottomButtons = False
937
938 # Add button bar: at the bottom of window (legacy mode), or share top row with user-name
939 if bottomButtons:
940 bbar = ttk.Frame(self.botpane)
941 bbar.pack(side='top', fill = 'x')
942 else:
943 bbar = self.toppane.user_frame
944
945 # Define help button
946 bbar.help_button = ttk.Button(bbar, text='Help',
947 command=self.help.open, style = 'normal.TButton')
948
949 # Define profile settings button
950 bbar.profile_button = ttk.Button(bbar, text='Settings',
951 command=self.profile.open, style = 'normal.TButton')
952
953 # Define the "quit" button and action
954 bbar.quit_button = ttk.Button(bbar, text='Quit', command=self.on_quit,
955 style = 'normal.TButton')
956 # Tool tips for button bar
957 tooltip.ToolTip(bbar.quit_button, text="Exit the project manager")
958 tooltip.ToolTip(bbar.help_button, text="Show help window")
959
960 if bottomButtons:
961 bbar.help_button.pack(side = 'left', padx = 5)
962 bbar.profile_button.pack(side = 'left', padx = 5)
963 bbar.quit_button.pack(side = 'right', padx = 5)
964 else:
965 # quit at TR like window-title's close; help towards the outside, settings towards inside
966 bbar.quit_button.pack(side = 'right', padx = 5)
967 bbar.help_button.pack(side = 'right', padx = 5)
968 bbar.profile_button.pack(side = 'right', padx = 5)
969
970 # Add the panes once the internal geometry is known
971 pane.add(self.toppane)
972 pane.add(self.botpane)
973 pane.paneconfig(self.toppane, stretch='first')
974 # self.update_idletasks()
975
976 #---------------------------------------------------------------
977 # Project list
978 # projects = os.listdir(os.path.expanduser('~/' + designdir))
979 # self.cur_project.set(projects[0])
980 # self.design_select = ttk.OptionMenu(self, self.cur_project, projects[0], *projects,
981 # style='blue.TMenubutton')
982
983 # New import list
984 # self.import_select = ttk.Button(self, text=self.cur_import, command=self.choose_import)
985
986 #---------------------------------------------------------
987 # Define project design actions
988 # self.design_actions = ttk.Frame(self)
989 # self.design_actions.characterize = ttk.Button(self.design_actions,
990 # text='Upload and Characterize', command=self.characterize)
991 # self.design_actions.characterize.grid(column = 0, row = 0)
992
993 # Define import actions
994 # self.import_actions = ttk.Frame(self)
995 # self.import_actions.upload = ttk.Button(self.import_actions,
996 # text='Upload Challenge', command=self.make_challenge)
997 # self.import_actions.upload.grid(column = 0, row = 0)
998
999 self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000,
1000 0 if deferLoad else None) # do immediate forced refresh (1st in mainloop)
1001 # self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000)
1002
1003 # Redirect stdout and stderr to the console as the last thing to do. . .
1004 # Otherwise errors in the GUI get sucked into the void.
1005 self.stdout = sys.stdout
1006 self.stderr = sys.stderr
1007 sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
1008 sys.stderr = ConsoleText.StderrRedirector(self.text_box)
1009
1010 if message:
1011 print(message)
1012
1013 if self.prefs == {}:
1014 print("No user preferences file, using default settings.")
1015
1016 # helper for Profile to do live mods of some of the user-prefs (without restart projectManager):
1017 def setUsername(self, newname):
1018 self.toppane.user_frame.user.config(text=newname)
1019
1020 def refreshToolTips(self):
1021 if self.prefs['schemeditor'] == 'xcircuit':
1022 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor")
1023 elif self.prefs['schemeditor'] == 'xschem':
1024 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor")
1025 else:
1026 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor")
1027
1028 if self.prefs['layouteditor'] == 'klayout':
1029 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor")
1030 else:
1031 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor")
1032
1033 def config_path(self, path):
1034 #returns the directory that path contains between .config and .ef-config
1035 if (os.path.exists(path + '/.config')):
1036 return '/.config'
1037 elif (os.path.exists(path + '/.ef-config')):
1038 return '/.ef-config'
1039 raise FileNotFoundError('Neither '+path+'/.config nor '+path+'/.ef-config exists.')
1040
1041 #------------------------------------------------------------------------
1042 # Check if a name is blacklisted for being a project folder
1043 #------------------------------------------------------------------------
1044
1045 def blacklisted(self, dirname):
1046 # Blacklist: Do not show files of these names:
1047 blacklist = [importdir, 'ip', 'upload', 'export', 'lost+found']
1048 if dirname in blacklist:
1049 return True
1050 else:
1051 return False
1052
1053 def write_prefs(self):
1054 global prefsfile
1055
1056 if self.prefs:
1057 expprefsfile = os.path.expanduser(prefsfile)
1058 prefspath = os.path.split(expprefsfile)[0]
1059 if not os.path.exists(prefspath):
1060 os.makedirs(prefspath)
1061 with open(os.path.expanduser(prefsfile), 'w') as f:
1062 json.dump(self.prefs, f, indent = 4)
1063
1064 def read_prefs(self):
1065 global prefsfile
1066
1067 # Set all known defaults even if they are not in the JSON file so
1068 # that it is not necessary to check for the existence of the keyword
1069 # in the dictionary every time it is accessed.
1070 if 'fontsize' not in self.prefs:
1071 self.prefs['fontsize'] = 11
1072 userid = os.environ['USER']
1073 uid = ''
1074 username = userid
1075 self.prefs['username'] = username
1076
1077 '''
1078 if 'username' not in self.prefs:
1079
1080 #
1081 #EFABLESS PLATFORM
1082 p = subprocess.run(['/ef/apps/bin/withnet' ,
1083 og_config.apps_path + '/og_uid_service.py', userid],
1084 stdout = subprocess.PIPE)
1085 if p.stdout:
1086 uid_string = p.stdout.splitlines()[0].decode('utf-8')
1087 userspec = re.findall(r'[^"\s]\S*|".+?"', uid_string)
1088 if len(userspec) > 0:
1089 username = userspec[0].strip('"')
1090 # uid = userspec[1]
1091 # Note userspec[1] = UID and userspec[2] = role, useful
1092 # for future applications.
1093 else:
1094 username = userid
1095 else:
1096 username = userid
1097 self.prefs['username'] = username
1098 # self.prefs['uid'] = uid
1099 '''
1100 if 'schemeditor' not in self.prefs:
1101 self.prefs['schemeditor'] = 'electric'
1102
1103 if 'layouteditor' not in self.prefs:
1104 self.prefs['layouteditor'] = 'magic'
1105
1106 if 'magic-graphics' not in self.prefs:
1107 self.prefs['magic-graphics'] = 'X11'
1108
1109 if 'development' not in self.prefs:
1110 self.prefs['development'] = False
1111
1112 if 'devstdcells' not in self.prefs:
1113 self.prefs['devstdcells'] = False
1114
1115 # Any additional user preferences go above this line.
1116
1117 # Get user preferences from ~/design/.profile/prefs.json and use it to
1118 # overwrite default entries in self.prefs
1119 try:
1120 with open(os.path.expanduser(prefsfile), 'r') as f:
1121 prefsdict = json.load(f)
1122 for key in prefsdict:
1123 self.prefs[key] = prefsdict[key]
1124 except:
1125 # No preferences file, so create an initial one.
1126 if not os.path.exists(prefsfile):
1127 self.write_prefs()
1128
1129 # if 'User:' Label exists, this updates it live (Profile calls read_prefs after write)
1130 try:
1131 self.setUsername(self.prefs['username'])
1132 except:
1133 pass
1134
1135 #------------------------------------------------------------------------
1136 # Get a list of the projects in the user's design directory. Exclude
1137 # items that are not directories, or which are blacklisted.
1138 #------------------------------------------------------------------------
1139
1140 def get_project_list(self):
1141 global importdir
1142
1143 badrex1 = re.compile("^\.")
1144 badrex2 = re.compile(".*[ \t\n].*")
1145
1146 # Get contents of directory. Look only at directories
1147 projectlist = list(item for item in os.listdir(self.projectdir) if
1148 os.path.isdir(self.projectdir + '/' + item))
1149
1150 # 'import' and others in the blacklist are not projects!
1151 # Files beginning with '.' and files with whitespace are
1152 # also not listed.
1153 for item in projectlist[:]:
1154 if self.blacklisted(item):
1155 projectlist.remove(item)
1156 elif badrex1.match(item):
1157 projectlist.remove(item)
1158 elif badrex2.match(item):
1159 projectlist.remove(item)
1160
1161 # Add pathname to all items in projectlist
1162 projectlist = [self.projectdir + '/' + item for item in projectlist]
1163 return projectlist
1164
1165 #------------------------------------------------------------------------
1166 # Get a list of the projects in the user's cloudv directory. Exclude
1167 # items that are not directories, or which are blacklisted.
1168 #------------------------------------------------------------------------
1169
1170 def get_cloudv_project_list(self):
1171 global importdir
1172
1173 badrex1 = re.compile("^\.")
1174 badrex2 = re.compile(".*[ \t\n].*")
1175
1176 if not os.path.exists(self.cloudvdir):
1177 print('No user cloudv dir exists; no projects to import.')
1178 return None
1179
1180 # Get contents of cloudv directory. Look only at directories
1181 projectlist = list(item for item in os.listdir(self.cloudvdir) if
1182 os.path.isdir(self.cloudvdir + '/' + item))
1183
1184 # 'import' and others in the blacklist are not projects!
1185 # Files beginning with '.' and files with whitespace are
1186 # also not listed.
1187 for item in projectlist[:]:
1188 if self.blacklisted(item):
1189 projectlist.remove(item)
1190 elif badrex1.match(item):
1191 projectlist.remove(item)
1192 elif badrex2.match(item):
1193 projectlist.remove(item)
1194
1195 # Add pathname to all items in projectlist
1196 projectlist = [self.cloudvdir + '/' + item for item in projectlist]
1197 return projectlist
1198
1199 #------------------------------------------------------------------------
1200 # utility: [re]intialize a project's elec/ dir: the .java preferences and LIBDIRS.
1201 # So user can just delete .java, and restart electric (from projectManager), to reinit preferences.
1202 # So user can just delete LIBDIRS, and restart electric (from projectManager), to reinit LIBDIRS.
1203 # So project copies/imports can filter ngspice/run (and ../.allwaves), we'll recreate it here.
1204 #
1205 # The global /ef/efabless/deskel/* is used and the PDK name substituted.
1206 #
1207 # This SINGLE function is used to setup elec/ contents for new projects, in addition to being
1208 # called in-line prior to "Edit Schematics" (on-the-fly).
1209 #------------------------------------------------------------------------
1210 @classmethod
1211 def reinitElec(cls, design):
1212 pdkdir = os.path.join( design, ".ef-config/techdir")
1213 elec = os.path.join( design, "elec")
1214
1215 # on the fly, ensure has elec/ dir, ensure has ngspice/run/allwaves dir
1216 try:
1217 os.makedirs(design + '/elec', exist_ok=True)
1218 except IOError as e:
1219 print('Error in os.makedirs(elec): ' + str(e))
1220 try:
1221 os.makedirs(design + '/ngspice/run/.allwaves', exist_ok=True)
1222 except IOError as e:
1223 print('Error in os.makedirs(.../.allwaves): ' + str(e))
1224 #EFABLESS PLATFORM
1225 deskel = '/ef/efabless/deskel'
1226
1227 # on the fly:
1228 # .../elec/.java : reinstall if missing. From PDK-specific if any.
1229 if not os.path.exists( os.path.join( elec, '.java')):
1230 # Copy Electric preferences
1231 try:
1232 shutil.copytree(deskel + '/dotjava', design + '/elec/.java', symlinks = True)
1233 except IOError as e:
1234 print('Error copying files: ' + str(e))
1235
1236 # .../elec/LIBDIRS : reinstall if missing, from PDK-specific LIBDIRS
1237 # in libs.tech/elec/LIBDIRS
1238
1239 libdirsloc = pdkdir + '/libs.tech/elec/LIBDIRS'
1240
1241 if not os.path.exists( os.path.join( elec, 'LIBDIRS')):
1242 if os.path.exists( libdirsloc ):
1243 # Copy Electric LIBDIRS
1244 try:
1245 shutil.copy(libdirsloc, design + '/elec/LIBDIRS')
1246 except IOError as e:
1247 print('Error copying files: ' + str(e))
1248 else:
1249 print('Info: PDK not configured for Electric: no libs.tech/elec/LIBDIRS')
1250
1251 return None
1252
1253 #------------------------------------------------------------------------
1254 # utility: filter a list removing: empty strings, strings with any whitespace
1255 #------------------------------------------------------------------------
1256 whitespaceREX = re.compile('\s')
1257 @classmethod
1258 def filterNullOrWS(cls, inlist):
1259 return [ i for i in inlist if i and not cls.whitespaceREX.search(i) ]
1260
1261 #------------------------------------------------------------------------
1262 # utility: do a glob.glob of relative pattern, but specify the rootDir,
1263 # so returns the matching paths found below that rootDir.
1264 #------------------------------------------------------------------------
1265 @classmethod
1266 def globFromDir(cls, pattern, dir=None):
1267 if dir:
1268 dir = dir.rstrip('/') + '/'
1269 pattern = dir + pattern
1270 result = glob.glob(pattern)
1271 if dir and result:
1272 nbr = len(dir)
1273 result = [ i[nbr:] for i in result ]
1274 return result
1275
1276 #------------------------------------------------------------------------
1277 # utility: from a pdkPath, return list of 3 strings: <foundry>, <node>, <description>.
1278 # i.e. pdkPath has form '[.../]<foundry>[.<ext>]/<node>'. For now the description
1279 # is always ''. And an optional foundry extension is pruned/dropped.
1280 # thus '.../XFAB.2/EFXP018A4' -> 'XFAB', 'EFXP018A4', ''
1281 #
1282 # optionally store in each PDK: .ef-config/nodeinfo.json which can define keys:
1283 # 'foundry', 'node', 'description' to override the foundry (computed from the path)
1284 # and (fixed, empty) description currently returned by this.
1285 #
1286 # Intent: keep a short-description field at least, intended to be one-line max 40 chars,
1287 # suitable for a on-hover-tooltip display. (Distinct from a big multiline description).
1288 #
1289 # On error (malformed pdkPath: can't determine foundry or node), the foundry or node
1290 # or both may be '' or as specified in the optional default values (if you're
1291 # generating something for display and want an unknown to appear as 'none' etc.).
1292 #------------------------------------------------------------------------
1293 @classmethod
1294 def pdkdir2fnd(cls, pdkdir, def_foundry='', def_node='', def_description=''):
1295 foundry = ''
1296 node = ''
1297 description = ''
1298 status = 'active'
1299 if pdkdir:
1300 split = os.path.split(os.path.realpath(pdkdir))
1301 # Full path should be [<something>/]<foundry>[.ext]/<node>
1302 node = split[1]
1303 foundry = os.path.split(split[0])[1]
1304 foundry = os.path.splitext(foundry)[0]
1305 # Check for nodeinfo.json
1306 infofile = pdkdir + '/.config/nodeinfo.json'
1307 if os.path.exists(infofile):
1308 with open(infofile, 'r') as ifile:
1309 nodeinfo = json.load(ifile)
1310 if 'foundry' in nodeinfo:
1311 foundry = nodeinfo['foundry']
1312 if 'node' in nodeinfo:
1313 node = nodeinfo['node']
1314 if 'description' in nodeinfo:
1315 description = nodeinfo['description']
1316 if 'status' in nodeinfo:
1317 status = nodeinfo['status']
1318 return foundry, node, description, status
1319
1320 infofile = pdkdir + '/.ef-config/nodeinfo.json'
1321 if os.path.exists(infofile):
1322 with open(infofile, 'r') as ifile:
1323 nodeinfo = json.load(ifile)
1324 if 'foundry' in nodeinfo:
1325 foundry = nodeinfo['foundry']
1326 if 'node' in nodeinfo:
1327 node = nodeinfo['node']
1328 if 'description' in nodeinfo:
1329 description = nodeinfo['description']
1330 if 'status' in nodeinfo:
1331 status = nodeinfo['status']
1332
1333
1334 return foundry, node, description, status
1335
1336 #------------------------------------------------------------------------
1337 # Get a list of the electric-libraries (DELIB only) in a given project.
1338 # List of full-paths each ending in '.delib'
1339 #------------------------------------------------------------------------
1340
1341 def get_elecLib_list(self, pname):
1342 elibs = self.globFromDir(pname + '/elec/*.delib/', self.projectdir)
1343 elibs = [ re.sub("/$", "", i) for i in elibs ]
1344 return self.filterNullOrWS(elibs)
1345
1346 #------------------------------------------------------------------------
1347 # Create a list of datestamps for each import file
1348 #------------------------------------------------------------------------
1349 def get_date_list(self, valuelist):
1350 datelist = []
1351 for value in valuelist:
1352 try:
1353 importfile = value[0]
1354 try:
1355 statbuf = os.stat(importfile)
1356 except:
1357 # Note entries that can't be accessed.
1358 datelist.append("(unknown)")
1359 else:
1360 datestamp = datetime.datetime.fromtimestamp(statbuf.st_mtime)
1361 datestr = datestamp.strftime("%c")
1362 datelist.append(datestr)
1363 except:
1364 datelist.append("(N/A)")
1365
1366 return datelist
1367
1368 #------------------------------------------------------------------------
1369 # Get the PDK attached to a project for display as: '<foundry> : <node>'
1370 # unless path=True: then return true PDK dir-path.
1371 #
1372 # TODO: the ef-config prog output is not used below. Intent was use
1373 # ef-config to be the one official query for *any* project's PDK value, and
1374 # therein-only hide a built-in default for legacy projects without techdir symlink.
1375 # In below ef-config will always give an EF_TECHDIR, so that code-branch always
1376 # says '(default)', the ef-config subproc is wasted, and '(no PDK)' is never
1377 # reached.
1378 #------------------------------------------------------------------------
1379 def get_pdk_dir(self, project, path=False):
1380 pdkdir = os.path.realpath(project + self.config_path(project)+'/techdir')
1381 if path:
1382 return pdkdir
1383 foundry, node, desc, status = self.pdkdir2fnd( pdkdir )
1384 return foundry + ' : ' + node
1385 '''
1386 if os.path.isdir(project + '/.ef-config'):
1387 if os.path.exists(project + '/.ef-config/techdir'):
1388 pdkdir = os.path.realpath(project + '/.ef-config/techdir')
1389
1390 elif os.path.isdir(project + '/.config'):
1391 if os.path.exists(project + '/.config/techdir'):
1392 pdkdir = os.path.realpath(project + '/.config/techdir')
1393 if path:
1394 return pdkdir
1395 foundry, node, desc, status = self.pdkdir2fnd( pdkdir )
1396 return foundry + ' : ' + node
1397 '''
1398 '''
1399 if not pdkdir:
1400 # Run "ef-config" script for backward compatibility
1401 export = {'EF_DESIGNDIR': project}
1402 #EFABLESS PLATFORM
1403 p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
1404 stdout = subprocess.PIPE, env = export)
1405 config_out = p.stdout.splitlines()
1406 for line in config_out:
1407 setline = line.decode('utf-8').split('=')
1408 if setline[0] == 'EF_TECHDIR':
1409 pdkdir = ( setline[1] if path else '(default)' )
1410 if not pdkdir:
1411 pdkdir = ( None if path else '(no PDK)' ) # shouldn't get here
1412 '''
1413
1414
1415
1416 return pdkdir
1417
1418 #------------------------------------------------------------------------
1419 # Get the list of PDKs that are attached to each project
1420 #------------------------------------------------------------------------
1421 def get_pdk_list(self, projectlist):
1422 pdklist = []
1423 for project in projectlist:
1424 pdkdir = self.get_pdk_dir(project)
1425 pdklist.append(pdkdir)
1426
1427 return pdklist
1428
1429 #------------------------------------------------------------------------
1430 # Find a .json's associated tar.gz (or .tgz) if any.
1431 # Return path to the tar.gz if any, else None.
1432 #------------------------------------------------------------------------
1433
1434 def json2targz(self, jsonPath):
1435 root = os.path.splitext(jsonPath)[0]
1436 for ext in ('.tgz', '.tar.gz'):
1437 if os.path.isfile(root + ext):
1438 return root + ext
1439 return None
1440
1441 #------------------------------------------------------------------------
1442 # Remove a .json and associated tar.gz (or .tgz) if any.
1443 # If not a .json, remove just that file (no test for a tar).
1444 #------------------------------------------------------------------------
1445
1446 def removeJsonPlus(self, jsonPath):
1447 ext = os.path.splitext(jsonPath)[1]
1448 if ext == ".json":
1449 tar = self.json2targz(jsonPath)
1450 if tar: os.remove(tar)
1451 return os.remove(jsonPath)
1452
1453 #------------------------------------------------------------------------
1454 # MOVE a .json and associated tar.gz (or .tgz) if any, to targetDir.
1455 # If not a .json, move just that file (no test for a tar).
1456 #------------------------------------------------------------------------
1457
1458 def moveJsonPlus(self, jsonPath, targetDir):
1459 ext = os.path.splitext(jsonPath)[1]
1460 if ext == ".json":
1461 tar = self.json2targz(jsonPath)
1462 if tar:
1463 shutil.move(tar, targetDir)
1464 # believe the move throws an error. So return value (the targetDir name) isn't really useful.
1465 return shutil.move(jsonPath, targetDir)
1466
1467 #------------------------------------------------------------------------
1468 # Get a list of the libraries in the user's ip folder
1469 #------------------------------------------------------------------------
1470
1471 def get_library_list(self):
1472 # Get contents of directory
1473 try:
1474 iplist = glob.glob(self.projectdir + '/ip/*/*')
1475 except:
1476 iplist = []
1477 else:
1478 pass
1479
1480 return iplist
1481
1482 #------------------------------------------------------------------------
1483 # Get a list of the files in the user's design import folder
1484 # (use current 'import' but also original 'upload')
1485 #------------------------------------------------------------------------
1486
1487 def get_import_list(self):
1488 # Get contents of directory
1489 importlist = os.listdir(self.projectdir + '/' + importdir)
1490
1491 # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz).
1492 # Also ignore any .swp files dropped by the vim editor.
1493 # Also ignore any subdirectories of import
1494 for item in importlist[:]:
1495 if item[-1] in '#~':
1496 importlist.remove(item)
1497 continue
1498 ipath = self.projectdir + '/' + importdir + '/' + item
1499
1500 # recognize dirs (as u2u projects) if not symlink and has a 'project.json',
1501 # hide dirs named *.bak. If originating user does u2u twice before target user
1502 # can consume/import it, the previous one (only) is retained as *.bak.
1503 if os.path.isdir(ipath):
1504 if os.path.islink(ipath) or not self.validProjectName(item) \
1505 or self.importProjNameBadrex1.match(item) \
1506 or not os.path.isfile(ipath + '/project.json'):
1507 importlist.remove(item)
1508 continue
1509 else:
1510 ext = os.path.splitext(item)
1511 if ext[1] == '.json':
1512 if ext[0] + '.tar.gz' in importlist:
1513 importlist.remove(ext[0] + '.tar.gz')
1514 elif ext[0] + '.tgz' in importlist:
1515 importlist.remove(ext[0] + '.tgz')
1516 elif ext[1] == '.swp':
1517 importlist.remove(item)
1518 elif os.path.isdir(self.projectdir + '/' + importdir + '/' + item):
1519 importlist.remove(item)
1520
1521 # Add pathname to all items in projectlist
1522 importlist = [self.projectdir + '/' + importdir + '/' + item for item in importlist]
1523
1524 # Add support for original "upload" directory (backward compatibility)
1525 if os.path.exists(self.projectdir + '/upload'):
1526 uploadlist = os.listdir(self.projectdir + '/upload')
1527
1528 # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz).
1529 # Also ignore any .swp files dropped by the vim editor.
1530 for item in uploadlist[:]:
1531 ext = os.path.splitext(item)
1532 if ext[1] == '.json':
1533 if ext[0] + '.tar.gz' in uploadlist:
1534 uploadlist.remove(ext[0] + '.tar.gz')
1535 elif ext[0] + '.tgz' in uploadlist:
1536 uploadlist.remove(ext[0] + '.tgz')
1537 elif ext[1] == '.swp':
1538 uploadlist.remove(item)
1539
1540 # Add pathname to all items in projectlist
1541 uploadlist = [self.projectdir + '/upload/' + item for item in uploadlist]
1542 importlist.extend(uploadlist)
1543
1544 # Remember the size of the list so we know when it changed
1545 self.number_of_imports = len(importlist)
1546 return importlist
1547
1548 #------------------------------------------------------------------------
1549 # Import for json documents and related tarballs (.gz or .tgz):
1550 #------------------------------------------------------------------------
1551
1552 def importjson(self, projname, importfile):
1553 # (1) Check if there is a tarball with the same root name as the JSON
1554 importroot = os.path.splitext(importfile)[0]
1555 badrex1 = re.compile("^\.")
1556 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
1557 if os.path.isfile(importroot + '.tgz'):
1558 tarname = importroot + '.tgz'
1559 elif os.path.isfile(importroot + '.tar.gz'):
1560 tarname = importroot + '.tar.gz'
1561 else:
1562 tarname = []
1563 # (2) Check for name conflict
1564 origname = projname
1565 newproject = self.projectdir + '/' + projname
1566 newname = projname
1567 while os.path.isdir(newproject) or self.blacklisted(newname):
1568 if self.blacklisted(newname):
1569 warning = "Name " + newname + " is not allowed for a project name."
1570 elif badrex1.match(newname):
1571 warning = 'project name may not start with "."'
1572 elif badrex2.match(newname):
1573 warning = 'project name contains illegal characters or whitespace.'
1574 else:
1575 warning = "Project " + newname + " already exists!"
1576 newname = ProjectNameDialog(self, warning, seed=newname).result
1577 if not newname:
1578 return 0 # Canceled, no action.
1579 newproject = self.projectdir + '/' + newname
1580 print("New project name is " + newname + ".")
1581 # (3) Create new directory
1582 os.makedirs(newproject)
1583 # (4) Dump the tarball (if any) in the new directory
1584 if tarname:
1585 with tarfile.open(tarname, mode='r:gz') as archive:
1586 for member in archive:
1587 archive.extract(member, newproject)
1588 # (5) Copy the JSON document into the new directory. Keep the
1589 # original name of the project, so as to overwrite any existing
1590 # document, then change the name to match that of the project
1591 # folder.
1592 # New behavior 12/2018: JSON file is always called 'project.json'.
1593 # Also support legacy JSON name if it exists (don't generate files with
1594 # both names)
1595
1596 jsonfile = newproject + '/project.json'
1597 if not os.path.isfile(jsonfile):
1598 if os.path.isfile(newproject + '/' + projname + '.json'):
1599 jsonfile = newproject + '/' + projname + '.json'
1600
1601 try:
1602 shutil.copy(importfile, jsonfile)
1603 except IOError as e:
1604 print('Error copying files: ' + str(e))
1605 return None
1606 else:
1607 # If filename is 'project.json' then it does not need to be changed.
1608 # This is for legacy name support only.
1609 if jsonfile != newproject + '/project.json':
1610 shutil.move(jsonfile, newproject + '/' + newname + '.json')
1611
1612 # (6) Remove the original files from the import folder
1613 os.remove(importfile)
1614 if tarname:
1615 os.remove(tarname)
1616
1617 # (7) Standard project setup: if spi/, elec/, and ngspice/ do not
1618 # exist, create them. If elec/.java does not exist, create it and
1619 # seed from deskel. If ngspice/run and ngspice/run/.allwaves do not
1620 # exist, create them.
1621
1622 if not os.path.exists(newproject + '/spi'):
1623 os.makedirs(newproject + '/spi')
1624 if not os.path.exists(newproject + '/spi/pex'):
1625 os.makedirs(newproject + '/spi/pex')
1626 if not os.path.exists(newproject + '/spi/lvs'):
1627 os.makedirs(newproject + '/spi/lvs')
1628 if not os.path.exists(newproject + '/ngspice'):
1629 os.makedirs(newproject + '/ngspice')
1630 if not os.path.exists(newproject + '/ngspice/run'):
1631 os.makedirs(newproject + '/ngspice/run')
1632 if not os.path.exists(newproject + '/ngspice/run/.allwaves'):
1633 os.makedirs(newproject + '/ngspice/run/.allwaves')
1634 if not os.path.exists(newproject + '/elec'):
1635 os.makedirs(newproject + '/elec')
1636 if not os.path.exists(newproject + '/xcirc'):
1637 os.makedirs(newproject + '/xcirc')
1638 if not os.path.exists(newproject + '/mag'):
1639 os.makedirs(newproject + '/mag')
1640
1641 self.reinitElec(newproject) # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any
1642
1643 return 1 # Success
1644
1645 #------------------------------------------------------------------------
1646 # Import for netlists (.spi):
1647 # (1) Request project name
1648 # (2) Create new project if name does not exist, or
1649 # place netlist in existing project if it does.
1650 #------------------------------------------------------------------------
1651
1652 #--------------------------------------------------------------------
1653 # Install netlist in electric:
1654 # "importfile" is the filename in ~/design/import
1655 # "pname" is the name of the target project (folder)
1656 # "newfile" is the netlist file name (which may or may not be the same
1657 # as 'importfile').
1658 #--------------------------------------------------------------------
1659
1660 def install_in_electric(self, importfile, pname, newfile, isnew=True):
1661 #--------------------------------------------------------------------
1662 # Install the netlist.
1663 # If netlist is CDL, then call cdl2spi first
1664 #--------------------------------------------------------------------
1665
1666 newproject = self.projectdir + '/' + pname
1667 if not os.path.isdir(newproject + '/spi/'):
1668 os.makedirs(newproject + '/spi/')
1669 if os.path.splitext(newfile)[1] == '.cdl':
1670 if not os.path.isdir(newproject + '/cdl/'):
1671 os.makedirs(newproject + '/cdl/')
1672 shutil.copy(importfile, newproject + '/cdl/' + newfile)
1673 try:
1674 p = subprocess.run(['/ef/apps/bin/cdl2spi', importfile],
1675 stdout = subprocess.PIPE, stderr = subprocess.PIPE,
1676 check = True)
1677 except subprocess.CalledProcessError as e:
1678 print('Error running cdl2spi: ' + e.output.decode('utf-8'))
1679 if isnew == True:
1680 shutil.rmtree(newproject)
1681 return None
1682 else:
1683 spi_string = p.stdout.splitlines()[0].decode('utf-8')
1684 if p.stderr:
1685 err_string = p.stderr.splitlines()[0].decode('utf-8')
1686 # Print error messages to console
1687 print(err_string)
1688 if not spi_string:
1689 print('Error: cdl2spi has no output')
1690 if isnew == True:
1691 shutil.rmtree(newproject)
1692 return None
1693 outname = os.path.splitext(newproject + '/spi/' + newfile)[0] + '.spi'
1694 with open(outname, 'w') as f:
1695 f.write(spi_string)
1696 else:
1697 outname = newproject + '/spi/' + newfile
1698 try:
1699 shutil.copy(importfile, outname)
1700 except IOError as e:
1701 print('Error copying files: ' + str(e))
1702 if isnew == True:
1703 shutil.rmtree(newproject)
1704 return None
1705
1706 #--------------------------------------------------------------------
1707 # Symbol generator---this code to be moved into its own def.
1708 #--------------------------------------------------------------------
1709 # To-do, need a more thorough SPICE parser, maybe use netgen to parse.
1710 # Need to find topmost subcircuit, by parsing the hieararchy.
1711 subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE)
1712 subnames = []
1713 with open(importfile, 'r') as f:
1714 for line in f:
1715 lmatch = subcktrex.match(line)
1716 if lmatch:
1717 subnames.append(lmatch.group(1))
1718
1719 if subnames:
1720 subname = subnames[0]
1721
1722 # Run cdl2icon perl script
1723 try:
1724 p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname',
1725 subname, '-libname', pname, '-projname', pname, '--prntgussddirs'],
1726 stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True)
1727 except subprocess.CalledProcessError as e:
1728 print('Error running cdl2spi: ' + e.output.decode('utf-8'))
1729 return None
1730 else:
1731 pin_string = p.stdout.splitlines()[0].decode('utf-8')
1732 if not pin_string:
1733 print('Error: cdl2icon has no output')
1734 if isnew == True:
1735 shutil.rmtree(newproject)
1736 return None
1737 if p.stderr:
1738 err_string = p.stderr.splitlines()[0].decode('utf-8')
1739 print(err_string)
1740
1741 # Invoke dialog to arrange pins here
1742 pin_info_list = SymbolBuilder(self, pin_string.split(), fontsize=self.prefs['fontsize']).result
1743 if not pin_info_list:
1744 # Dialog was canceled
1745 print("Symbol builder was canceled.")
1746 if isnew == True:
1747 shutil.rmtree(newproject)
1748 return 0
1749
1750 for pin in pin_info_list:
1751 pin_info = pin.split(':')
1752 pin_name = pin_info[0]
1753 pin_type = pin_info[1]
1754
1755 # Call cdl2icon with the final pin directions
1756 outname = newproject + '/elec/' + pname + '.delib/' + os.path.splitext(newfile)[0] + '.ic'
1757 try:
1758 p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname',
1759 subname, '-libname', pname, '-projname', pname, '-output',
1760 outname, '-pindircmbndstring', ','.join(pin_info_list)],
1761 stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True)
1762 except subprocess.CalledProcessError as e:
1763 print('Error running cdl2icon: ' + e.output.decode('utf-8'))
1764 if isnew == True:
1765 shutil.rmtree(newproject)
1766 return None
1767 else:
1768 icon_string = p.stdout.splitlines()[0].decode('utf-8') # not used, AFAIK
1769 if p.stderr:
1770 err_string = p.stderr.splitlines()[0].decode('utf-8')
1771 print(err_string)
1772
1773 return 1 # Success
1774
1775 #------------------------------------------------------------------------
1776 # Import netlist file into existing project
1777 #------------------------------------------------------------------------
1778
1779 def importspiceinto(self, newfile, importfile):
1780 # Require existing project location
1781 ppath = ExistingProjectDialog(self, self.get_project_list()).result
1782 if not ppath:
1783 return 0 # Canceled in dialog, no action.
1784 pname = os.path.split(ppath)[1]
1785 print("Importing into existing project " + pname)
1786 result = self.install_in_electric(importfile, pname, newfile, isnew=False)
1787 if result == None:
1788 print('Error during import.')
1789 return None
1790 elif result == 0:
1791 return 0 # Canceled
1792 else:
1793 # Remove original file from imports area
1794 os.remove(importfile)
1795 return 1 # Success
1796
1797 #------------------------------------------------------------------------
1798 # Import netlist file as a new project
1799 #------------------------------------------------------------------------
1800
1801 def importspice(self, newfile, importfile):
1802 # Use create project code first to generate a valid project space.
1803 newname = self.createproject(None)
1804 if not newname:
1805 return 0 # Canceled in dialog, no action.
1806 print("Importing as new project " + newname + ".")
1807 result = self.install_in_electric(importfile, newname, newfile, isnew=True)
1808 if result == None:
1809 print('Error during install')
1810 return None
1811 elif result == 0:
1812 # Canceled, so do not remove the import
1813 return 0
1814 else:
1815 # Remove original file from imports area
1816 os.remove(importfile)
1817 return 1 # Success
1818
1819 #------------------------------------------------------------------------
1820 # Determine if JSON's tar can be imported as-if it were just a *.v.
1821 # This is thin wrapper around tarVglImportable. Find the JSON's associated
1822 # tar.gz if any, and call tarVglImportable.
1823 # Returns list of two:
1824 # None if rules not satisified; else path of the single GL .v member.
1825 # None if rules not satisified; else root-name of the single .json member.
1826 #------------------------------------------------------------------------
1827
1828 def jsonTarVglImportable(self, path):
1829 ext = os.path.splitext(path)[1]
1830 if ext != '.json': return None, None, None
1831
1832 tar = self.json2targz(path)
1833 if not tar: return None, None, None
1834
1835 return self.tarVglImportable(tar)
1836
1837 #------------------------------------------------------------------------
1838 # Get a single named member (memPath) out of a JSON's tar file.
1839 # This is thin wrapper around tarMember2tempfile. Find the JSON's associated
1840 # tar.gz if any, and call tarMember2tempfile.
1841 #------------------------------------------------------------------------
1842
1843 def jsonTarMember2tempfile(self, path, memPath):
1844 ext = os.path.splitext(path)[1]
1845 if ext != '.json': return None
1846
1847 tar = self.json2targz(path)
1848 if not tar: return None
1849
1850 return self.tarMember2tempfile(tar, memPath)
1851
1852 #------------------------------------------------------------------------
1853 # Determine if tar-file can be imported as-if it were just a *.v.
1854 # Require exactly one yosys-output .netlist.v, and exactly one .json.
1855 # Nothing else matters: Ignore all other *.v, *.tv, *.jelib, *.vcd...
1856 #
1857 # If user renames *.netlist.v in cloudv before export to not end in
1858 # netlist.v, we won't recognize it.
1859 #
1860 # Returns list of two:
1861 # None if rules not satisified; else path of the single GL netlist.v member.
1862 # None if rules not satisified; else root-name of the single .json member.
1863 #------------------------------------------------------------------------
1864
1865 def tarVglImportable(self, path):
1866 # count tar members by extensions. Track the .netlist.v. and .json. Screw the rest.
1867 nbrExt = {'.v':0, '.netlist.v':0, '.tv':0, '.jelib':0, '.json':0, '/other/':0, '/vgl/':0}
1868 nbrGLv = 0
1869 jname = None
1870 vfile = None
1871 node = None
1872 t = tarfile.open(path)
1873 for i in t:
1874 # ignore (without counting) dir entries. From cloudv (so far) the tar does not
1875 # have dir-entries, but most tar do (esp. most manually made test cases).
1876 if i.isdir():
1877 continue
1878 # TODO: should we require all below counted files to be plain files (no symlinks etc.)?
1879 # get extension, but recognize a multi-ext for .netlist.v case
1880 basenm = os.path.basename(i.name)
1881 ext = os.path.splitext(basenm)[1]
1882 root = os.path.splitext(basenm)[0]
1883 ext2 = os.path.splitext(root)[1]
1884 if ext2 == '.netlist' and ext == '.v':
1885 ext = ext2 + ext
1886 if ext and ext not in nbrExt:
1887 ext = '/other/'
1888 elif ext == '.netlist.v' and self.tarMemberIsGLverilog(t, i.name):
1889 vfile = i.name
1890 ext = '/vgl/'
1891 elif ext == '.json':
1892 node = self.tarMemberHasFoundryNode(t, i.name)
1893 jname = root
1894 nbrExt[ext] += 1
1895
1896 # check rules. Require exactly one yosys-output .netlist.v, and exactly one .json.
1897 # Quantities of other types are all don't cares.
1898 if (nbrExt['/vgl/'] == 1 and nbrExt['.json'] == 1):
1899 # vfile is the name of the verilog netlist in the tarball, while jname
1900 # is the root name of the JSON file found in the tarball (if any)
1901 return vfile, jname, node
1902
1903 # failed, not gate-level-verilog importable:
1904 return None, None, node
1905
1906
1907 #------------------------------------------------------------------------
1908 # OBSOLETE VERSION: Determine if tar-file can be imported as-if it were just a *.v.
1909 # Rules for members: one *.v, {0,1} *.jelib, {0,1} *.json, 0 other types.
1910 # Return None if rules not satisified; else return path of the single .v.
1911 #------------------------------------------------------------------------
1912 #
1913 # def tarVglImportable(self, path):
1914 # # count tar members by extensions. Track the .v.
1915 # nbrExt = {'.v':0, '.jelib':0, '.json':0, 'other':0}
1916 # vfile = ""
1917 # t = tarfile.open(path)
1918 # for i in t:
1919 # ext = os.path.splitext(i.name)[1]
1920 # if ext not in nbrExt:
1921 # ext = 'other'
1922 # nbrExt[ext] += 1
1923 # if ext == ".v": vfile = i.name
1924 #
1925 # # check rules.
1926 # if (nbrExt['.v'] != 1 or nbrExt['other'] != 0 or
1927 # nbrExt['.jelib'] > 1 or nbrExt['.json'] > 1):
1928 # return None
1929 # return vfile
1930
1931 #------------------------------------------------------------------------
1932 # Get a single named member (memPath) out of a tar file (tarPath), into a
1933 # temp-file, so subprocesses can process it.
1934 # Return path to the temp-file, or None if member not found in the tar.
1935 #------------------------------------------------------------------------
1936
1937 def tarMember2tempfile(self, tarPath, memPath):
1938 t = tarfile.open(tarPath)
1939 member = t.getmember(memPath)
1940 if not member: return None
1941
1942 # Change member.name so it extracts into our new temp-file.
1943 # extract() can specify the root-dir befow which the member path
1944 # resides. If temp is an absolute-path, that root-dir must be /.
1945 tmpf1 = tempfile.NamedTemporaryFile(delete=False)
1946 if tmpf1.name[0] != "/":
1947 raise ValueError("assertion failed, temp-file path not absolute: %s" % tmpf1.name)
1948 member.name = tmpf1.name
1949 t.extract(member,"/")
1950
1951 return tmpf1.name
1952
1953 #------------------------------------------------------------------------
1954 # Create an electric .delib directory and seed it with a header file
1955 #------------------------------------------------------------------------
1956
1957 def create_electric_header_file(self, project, libname):
1958 if not os.path.isdir(project + '/elec/' + libname + '.delib'):
1959 os.makedirs(project + '/elec/' + libname + '.delib')
1960
1961 p = subprocess.run(['electric', '-v'], stdout=subprocess.PIPE)
1962 eversion = p.stdout.splitlines()[0].decode('utf-8')
1963 # Create header file
1964 with open(project + '/elec/' + libname + '.delib/header', 'w') as f:
1965 f.write('# header information:\n')
1966 f.write('H' + libname + '|' + eversion + '\n\n')
1967 f.write('# Tools:\n')
1968 f.write('Ouser|DefaultTechnology()Sschematic\n')
1969 f.write('Osimulation|VerilogUseAssign()BT\n')
1970 f.write('C____SEARCH_FOR_CELL_FILES____\n')
1971
1972 #------------------------------------------------------------------------
1973 # Create an ad-hoc "project.json" dictionary and fill essential records
1974 #------------------------------------------------------------------------
1975
1976 def create_ad_hoc_json(self, ipname, pname):
1977 # Create ad-hoc JSON file and fill it with the minimum
1978 # necessary entries to define a project.
1979 jData = {}
1980 jDS = {}
1981 jDS['ip-name'] = ipname
1982 pdkdir = self.get_pdk_dir(pname, path=True)
1983 try:
1984 jDS['foundry'], jDS['node'], pdk_desc, pdk_stat = self.pdkdir2fnd( pdkdir )
1985 except:
1986 # Cannot parse PDK name, so foundry and node will remain undefined
1987 pass
1988 jDS['format'] = '3'
1989 pparams = []
1990 param = {}
1991 param['unit'] = "\u00b5m\u00b2"
1992 param['condition'] = "device_area"
1993 param['display'] = "Device area"
1994 pmax = {}
1995 pmax['penalty'] = '0'
1996 pmax['target'] = '100000'
1997 param['max'] = pmax
1998 pparams.append(param)
1999
2000 param = {}
2001 param['unit'] = "\u00b5m\u00b2"
2002 param['condition'] = "area"
2003 param['display'] = "Layout area"
2004 pmax = {}
2005 pmax['penalty'] = '0'
2006 pmax['target'] = '100000'
2007 param['max'] = pmax
2008 pparams.append(param)
2009
2010 param = {}
2011 param['unit'] = "\u00b5m"
2012 param['condition'] = "width"
2013 param['display'] = "Layout width"
2014 pmax = {}
2015 pmax['penalty'] = '0'
2016 pmax['target'] = '300'
2017 param['max'] = pmax
2018 pparams.append(param)
2019
2020 param = {}
2021 param['condition'] = "DRC_errors"
2022 param['display'] = "DRC errors"
2023 pmax = {}
2024 pmax['penalty'] = 'fail'
2025 pmax['target'] = '0'
2026 param['max'] = pmax
2027 pparams.append(param)
2028
2029 param = {}
2030 param['condition'] = "LVS_errors"
2031 param['display'] = "LVS errors"
2032 pmax = {}
2033 pmax['penalty'] = 'fail'
2034 pmax['target'] = '0'
2035 param['max'] = pmax
2036 pparams.append(param)
2037
2038 jDS['physical-params'] = pparams
2039 jData['data-sheet'] = jDS
2040
2041 return jData
2042
2043 #------------------------------------------------------------------------
2044 # For a single named member (memPath) out of an open tarfile (tarf),
2045 # determine if it is a JSON file, and attempt to extract value of entry
2046 # 'node' in dictionary entry 'data-sheet'. Otherwise return None.
2047 #------------------------------------------------------------------------
2048
2049 def tarMemberHasFoundryNode(self, tarf, memPath):
2050 fileJSON = tarf.extractfile(memPath)
2051 if not fileJSON: return None
2052
2053 try:
2054 # NOTE: tarfile data is in bytes, json.load(fileJSON) does not work.
2055 datatop = json.loads(fileJSON.read().decode('utf-8'))
2056 except:
2057 print("Failed to load extract file " + memPath + " as JSON data")
2058 return None
2059 else:
2060 node = None
2061 if 'data-sheet' in datatop:
2062 dsheet = datatop['data-sheet']
2063 if 'node' in dsheet:
2064 node = dsheet['node']
2065
2066 fileJSON.close() # close open-tarfile before any return
2067 return node
2068
2069 #------------------------------------------------------------------------
2070 # For a single named member (memPath) out of an open tarfile (tarf),
2071 # determine if first line embeds (case-insensitive match): Generated by Yosys
2072 # Return True or False. If no such member or it has no 1st line, returns False.
2073 #------------------------------------------------------------------------
2074
2075 def tarMemberIsGLverilog(self, tarf, memPath):
2076 fileHdl = tarf.extractfile(memPath)
2077 if not fileHdl: return False
2078
2079 line = fileHdl.readline()
2080 fileHdl.close() # close open-tarfile before any return
2081 if not line: return False
2082 return ('generated by yosys' in line.decode('utf-8').lower())
2083
2084 #------------------------------------------------------------------------
2085 # Import vgl-netlist file INTO existing project.
2086 # The importfile can be a .v; or a .json-with-tar that embeds a .v.
2087 # What is newfile? not used here.
2088 #
2089 # PROMPT to select an existing project is here.
2090 # (Is also a PROMPT to select existing electric lib, but that's within importvgl).
2091 #------------------------------------------------------------------------
2092
2093 def importvglinto(self, newfile, importfile):
2094 # Require existing project location
2095 ppath = ExistingProjectDialog(self, self.get_project_list()).result
2096 if not ppath: return 0 # Canceled in dialog, no action.
2097 pname = os.path.split(ppath)[1]
2098 print( "Importing into existing project: %s" % (pname))
2099
2100 return self.importvgl(newfile, importfile, pname)
2101
2102 #------------------------------------------------------------------------
2103 # Import cloudv project as new project.
2104 #------------------------------------------------------------------------
2105
2106 def install_from_cloudv(self, opath, ppath, pdkname, stdcellname, ydicts):
2107 oname = os.path.split(opath)[1]
2108 pname = os.path.split(ppath)[1]
2109
2110 print('Cloudv project name is ' + str(oname))
2111 print('New Open Galaxy project name is ' + str(pname))
2112
2113 os.makedirs(ppath + '/verilog', exist_ok=True)
2114
2115 vfile = None
2116 isfullchip = False
2117 ipname = oname
2118
2119 # First check for single synthesized projects, or all synthesized
2120 # digital sub-blocks within a full-chip project.
2121
2122 os.makedirs(ppath + '/verilog/source', exist_ok=True)
2123 bfiles = glob.glob(opath + '/build/*.netlist.v')
2124 for bfile in bfiles:
2125 tname = os.path.split(bfile)[1]
2126 vname = os.path.splitext(os.path.splitext(tname)[0])[0]
2127 tfile = ppath + '/verilog/' + vname + '/' + vname + '.vgl'
2128 print('Making qflow sub-project ' + vname)
2129 os.makedirs(ppath + '/verilog/' + vname, exist_ok=True)
2130 shutil.copy(bfile, tfile)
2131 if vname == oname:
2132 vfile = tfile
2133
2134 # Each build project gets its own qflow directory. Create the
2135 # source/ subdirectory and make a link back to the .vgl file.
2136 # qflow prep should do the rest.
2137
2138 os.makedirs(ppath + '/qflow', exist_ok=True)
2139 os.makedirs(ppath + '/qflow/' + vname)
2140 os.makedirs(ppath + '/qflow/' + vname + '/source')
2141
2142 # Make sure the symbolic link is relative, so that it is portable
2143 # through a shared project.
2144 curdir = os.getcwd()
2145 os.chdir(ppath + '/qflow/' + vname + '/source')
2146 os.symlink('../../../verilog/' + vname + '/' + vname + '.vgl', vname + '.v')
2147 os.chdir(curdir)
2148
2149 # Create a simple qflow_vars.sh file so that the project manager
2150 # qflow launcher will see it as a qflow sub-project. If the meta.yaml
2151 # file has a "stdcell" entry for the subproject, then add the line
2152 # "techname=" with the name of the standard cell library as pulled
2153 # from meta.yaml.
2154
2155 stdcell = None
2156 buildname = 'build/' + vname + '.netlist.v'
2157 for ydict in ydicts:
2158 if buildname in ydict:
2159 yentry = ydict[buildname]
2160 if 'stdcell' in yentry:
2161 stdcell = yentry['stdcell']
2162
2163 with open(ppath + '/qflow/' + vname + '/qflow_vars.sh', 'w') as ofile:
2164 print('#!/bin/tcsh -f', file=ofile)
2165 if stdcell:
2166 print('set techname=' + stdcell, file=ofile)
2167
2168 # Now check for a full-chip verilog SoC (from CloudV)
2169
2170 modrex = re.compile('[ \t]*module[ \t]+[^ \t(]*_?soc[ \t]*\(')
2171 genmodrex = re.compile('[ \t]*module[ \t]+([^ \t(]+)[ \t]*\(')
2172
2173 bfiles = glob.glob(opath + '/*.model/*.v')
2174 for bfile in bfiles:
2175 tname = os.path.split(bfile)[1]
2176 vpath = os.path.split(bfile)[0]
2177 ipname = os.path.splitext(tname)[0]
2178 tfile = ppath + '/verilog/' + ipname + '.v'
2179 isfullchip = True
2180 break
2181
2182 if isfullchip:
2183 print('Cloudv project IP name is ' + str(ipname))
2184
2185 # All files in */ paths should be copied to project verilog/source/,
2186 # except for the module containing the SoC itself. Note that the actual
2187 # verilog source goes here, not the synthesized netlist, although that is
2188 # mainly for efficiency of the simulation, which would normally be done in
2189 # cloudV and not in Open Galaxy. For Open Galaxy, what is needed is the
2190 # existence of a verilog file containing a module name, which is used to
2191 # track down the various files (LEF, DEF, etc.) that are needed for full-
2192 # chip layout.
2193 #
2194 # (Sept. 2019) Added copying of files in /SW/ -> /sw/ and /Verify/ ->
2195 # /verify/ for running full-chip simulations on the Open Galaxy side.
2196
2197 os.makedirs(ppath + '/verilog', exist_ok=True)
2198
2199 cfiles = glob.glob(vpath + '/source/*')
2200 for cfile in cfiles:
2201 cname = os.path.split(cfile)[1]
2202 if cname != tname:
2203 tpath = ppath + '/verilog/source/' + cname
2204 os.makedirs(ppath + '/verilog/source', exist_ok=True)
2205 shutil.copy(cfile, tpath)
2206
2207 cfiles = glob.glob(vpath + '/verify/*')
2208 for cfile in cfiles:
2209 cname = os.path.split(cfile)[1]
2210 tpath = ppath + '/verilog/verify/' + cname
2211 os.makedirs(ppath + '/verilog/verify', exist_ok=True)
2212 shutil.copy(cfile, tpath)
2213
2214 cfiles = glob.glob(vpath + '/sw/*')
2215 for cfile in cfiles:
2216 cname = os.path.split(cfile)[1]
2217 tpath = ppath + '/verilog/sw/' + cname
2218 os.makedirs(ppath + '/verilog/sw', exist_ok=True)
2219 shutil.copy(cfile, tpath)
2220
2221 # Read the top-level SoC verilog and recast it for OpenGalaxy.
2222 with open(bfile, 'r') as ifile:
2223 chiplines = ifile.read().splitlines()
2224
2225 # Find the modules used, track them down, and add the source location
2226 # in the Open Galaxy environment as an "include" line in the top level
2227 # verilog.
2228
2229 parentdir = os.path.split(bfile)[0]
2230 modfile = parentdir + '/docs/modules.txt'
2231
2232 modules = []
2233 if os.path.isfile(modfile):
2234 with open(modfile, 'r') as ifile:
2235 modules = ifile.read().splitlines()
2236 else:
2237 print("Warning: No modules.txt file for the chip top level module in "
2238 + parentdir + "/docs/.\n")
2239
2240 # Get the names of verilog libraries in this PDK.
2241 pdkdir = os.path.realpath(ppath + '/.ef-config/techdir')
2242 pdkvlog = pdkdir + '/libs.ref/verilog'
2243 pdkvlogfiles = glob.glob(pdkvlog + '/*/*.v')
2244
2245 # Read the verilog libraries and create a dictionary mapping each
2246 # module name to a location of the verilog file where it is located.
2247 moddict = {}
2248 for vlogfile in pdkvlogfiles:
2249 with open(vlogfile, 'r') as ifile:
2250 for line in ifile.read().splitlines():
2251 mmatch = genmodrex.match(line)
2252 if mmatch:
2253 modname = mmatch.group(1)
2254 moddict[modname] = vlogfile
2255
2256 # Get the names of verilog libraries in the user IP space.
2257 # (TO DO: Need to know the IP version being used!)
2258 designdir = os.path.split(ppath)[0]
2259 ipdir = designdir + '/ip/'
2260 uservlogfiles = glob.glob(ipdir + '/*/*/verilog/*.v')
2261 for vlogfile in uservlogfiles:
2262 # Strip ipdir from the front
2263 vlogpath = vlogfile.replace(ipdir, '', 1)
2264 with open(vlogfile, 'r') as ifile:
2265 for line in ifile.read().splitlines():
2266 mmatch = genmodrex.match(line)
2267 if mmatch:
2268 modname = mmatch.group(1)
2269 moddict[modname] = vlogpath
2270
2271 # Find all netlist builds from the project (those that were copied above)
2272 buildfiles = glob.glob(ppath + '/verilog/source/*.v')
2273 for vlogfile in buildfiles:
2274 # Strip ipdir from the front
2275 vlogpath = vlogfile.replace(ppath + '/verilog/source/', '', 1)
2276 with open(vlogfile, 'r') as ifile:
2277 for line in ifile.read().splitlines():
2278 mmatch = genmodrex.match(line)
2279 if mmatch:
2280 modname = mmatch.group(1)
2281 moddict[modname] = vlogpath
2282
2283 # (NOTE: removing 'ifndef LVS' as netgen should be able to handle
2284 # the contents of included files, and they are preferred since any
2285 # arrays are declared in each module I/O)
2286 # chiplines.insert(0, '`endif')
2287 chiplines.insert(0, '//--- End of list of included module dependencies ---')
2288 includedfiles = []
2289 for module in modules:
2290 # Determine where this module comes from. Look in the PDK, then in
2291 # the user ip/ directory, then in the local hierarchy. Note that
2292 # the local hierarchy expects layouts from synthesized netlists that
2293 # have not yet been created, so determine the expected location.
2294
2295 if module in moddict:
2296 if moddict[module] not in includedfiles:
2297 chiplines.insert(0, '`include "' + moddict[module] + '"')
2298 includedfiles.append(moddict[module])
2299
2300 # chiplines.insert(0, '`ifndef LVS')
2301 chiplines.insert(0, '//--- List of included module dependencies ---')
2302 chiplines.insert(0, '// iverilog simulation requires the use of -I source -I ~/design/ip')
2303 chiplines.insert(0, '// NOTE: Includes may be rooted at ~/design/ip/ or at ./source')
2304 chiplines.insert(0, '// SoC top level verilog copied and modified by project manager')
2305
2306 # Copy file, but replace the module name "soc" with the ip-name
2307 with open(tfile, 'w') as ofile:
2308 for chipline in chiplines:
2309 print(modrex.sub('module ' + ipname + ' (', chipline), file=ofile)
2310
2311 # Need to define behavior: What if there is more than one netlist?
2312 # Which one is to be imported? For now, ad-hoc behavior is to select
2313 # the last netlist file in the list if no file matches the ip-name.
2314
2315 # Note that for full-chip projects, the full chip verilog file is always
2316 # the last one set.
2317
2318 if not vfile:
2319 try:
2320 vfile = tfile
2321 except:
2322 pass
2323
2324 # NOTE: vfile was being used to create a symbol, but not any more;
2325 # see below. All the above code referencing vfile can probably be
2326 # removed.
2327
2328 try:
2329 sfiles = glob.glob(vpath + '/source/*')
2330 sfiles.extend(glob.glob(vpath + '/*/source/*'))
2331 except:
2332 sfiles = glob.glob(opath + '/*.v')
2333 sfiles.extend(glob.glob(opath + '/*.sv'))
2334 sfiles.extend(glob.glob(opath + '/local/*'))
2335
2336 for fname in sfiles:
2337 sname = os.path.split(fname)[1]
2338 tfile = ppath + '/verilog/source/' + sname
2339 # Reject '.model' and '.soc" files (these are meaningful only to CloudV)
2340 fileext = os.path.splitext(fname)[1]
2341 if fileext == '.model' or fileext == '.soc':
2342 continue
2343 if os.path.isfile(fname):
2344 # Check if /verilog/source/ has been created
2345 if not os.path.isdir(ppath + '/verilog/source'):
2346 os.makedirs(ppath + '/verilog/source')
2347 shutil.copy(fname, tfile)
2348
2349 # Add standard cell library name to project.json
2350 pjsonfile = ppath + '/project.json'
2351 if os.path.exists(pjsonfile):
2352 with open(pjsonfile, 'r') as ifile:
2353 datatop = json.load(ifile)
2354 else:
2355 datatop = self.create_ad_hoc_json(ipname, ppath)
2356
2357 # Generate a symbol in electric for the verilog top module
2358 iconfile = ppath + '/elec/' + ipname + '.delib/' + ipname + '.ic'
2359 if not os.path.exists(iconfile):
2360 # NOTE: Symbols are created by qflow migration for project
2361 # builds. Only the chip top-level needs to run create_symbol
2362 # here.
2363
2364 if isfullchip:
2365 print("Creating symbol for module " + ipname + " automatically from verilog source.")
2366 create_symbol(ppath, vfile, ipname, iconfile, False)
2367 # Add header file
2368 self.create_electric_header_file(ppath, ipname)
2369
2370 dsheet = datatop['data-sheet']
2371 if not stdcellname or stdcellname == "":
2372 dsheet['standard-cell'] = 'default'
2373 else:
2374 dsheet['standard-cell'] = stdcellname
2375
2376 with open(pjsonfile, 'w') as ofile:
2377 json.dump(datatop, ofile, indent = 4)
2378
2379 return 0
2380
2381 #------------------------------------------------------------------------
2382 # Import vgl-netlist AS new project.
2383 # The importfile can be a .v; or a .json-with-tar that embeds a .v.
2384 # What is newfile? not used here.
2385 #
2386 # PROMPT to select an create new project is within importvgl.
2387 #------------------------------------------------------------------------
2388
2389 def importvglas(self, newfile, importfile, seedname):
2390 print('importvglas: seedname is ' + str(seedname))
2391 return self.importvgl(newfile, importfile, newname=None, seedname=seedname)
2392
2393 #------------------------------------------------------------------------
2394 # Utility shared/used by both: Import vgl-netlist file AS or INTO a project.
2395 # Called directly for AS. Called via importvglinto for INTO.
2396 # importfile : source of .v to import, actual .v or json-with-tar that embeds a .v
2397 # newfile : not used
2398 # newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
2399 # Either newname is given: we PROMPT to pick an existing elecLib;
2400 # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
2401 #------------------------------------------------------------------------
2402
2403 def importvgl(self, newfile, importfile, newname=None, seedname=None):
2404 elecLib = None
2405 isnew = not newname
2406
2407 # Up front: Determine if this import has a .json file associated
2408 # with it. If so, then parse the JSON data to find if there is a
2409 # foundry and node set for the project. If so, then the foundry
2410 # node is not selectable at time of import. Likewise, if "isnew"
2411 # is false, then we need to check if there is a directory called
2412 # "newname" and if it is set to the same foundry node. If not,
2413 # then the import must be rejected.
2414
2415 tarVfile, jName, importnode = self.jsonTarVglImportable(importfile)
2416
2417 if isnew:
2418 print('importvgl: seedname is ' + str(seedname))
2419 # Use create project code first to generate a valid project space.
2420 newname = self.createproject(None, seedname, importnode)
2421 if not newname: return 0 # Canceled in dialog, no action.
2422 print("Importing as new project " + newname + ".")
2423 elecLib = newname
2424
2425 ppath = self.projectdir + '/' + newname
2426 if not elecLib:
2427 choices = self.get_elecLib_list(newname)
2428 if not choices:
2429 print( "Aborted: No existing electric libraries found to import into.")
2430 return 0
2431
2432 elecLib = ExistingElecLibDialog(self, choices).result
2433 if not elecLib:
2434 # Never a just-created project to delete here: We only PROMPT to pick elecLib in non-new case.
2435 return 0 # Canceled in dialog, no action.
2436
2437 # Isolate just electric lib name without extension. ../a/b.delib -> b
2438 elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
2439 print("Importing to project: %s, elecLib: %s" % (newname, elecLib))
2440
2441 # Determine isolated *.v as importactual. May be importfile or tar-member (as temp-file).
2442 importactual = importfile
2443 if tarVfile:
2444 importactual = self.jsonTarMember2tempfile(importfile, tarVfile)
2445 print("importing json-with-tar's member: %s" % (tarVfile))
2446
2447 if not os.path.isfile(importactual):
2448 # TODO: should this be a raise instead?
2449 print('Error determining *.v to import')
2450 return None
2451
2452 result = self.vgl_install(importactual, newname, elecLib, newfile, isnew=isnew)
2453 if result == None:
2454 print('Error during install')
2455 return None
2456 elif result == 0:
2457 # Canceled, so do not remove the import
2458 return 0
2459 else:
2460 # If jName is non-NULL then there is a JSON file in the tarball. This is
2461 # to be used as the project JSON file. Contents of file coming from
2462 # CloudV are correct as of 12/8/2017.
2463 pname = os.path.expanduser('~/design/' + newname)
2464 legacyjname = pname + '/' + newname + '.json'
2465 # New behavior 12/2018: Project JSON file always named 'project.json'
2466 jname = pname + '/project.json'
2467
2468 # Do not overwrite an existing JSON file. Overwriting is a problem for
2469 # "import into", as the files go into an existing project, which would
2470 # normally have its own JSON file.
2471
2472 if not os.path.exists(jname) and not os.path.exists(legacyjname):
2473 try:
2474 tarJfile = os.path.split(tarVfile)[0] + '/' + jName + '.json'
2475 importjson = self.jsonTarMember2tempfile(importfile, tarJfile)
2476 except:
2477 jData = self.create_ad_hoc_json(newname, pname)
2478
2479 with open(jname, 'w') as ofile:
2480 json.dump(jData, ofile, indent = 4)
2481
2482 else:
2483 # Copy the temporary file pulled from the tarball and
2484 # remove the temporary file.
2485 shutil.copy(importjson, jname)
2486 os.remove(importjson)
2487
2488 # For time-being, if a tar.gz & json: archive them in the target project, also as extracted.
2489 # Remove original file from imports area (either .v; or .json plus tar)
2490 # plus temp-file if extracted from the tar.
2491 if importactual != importfile:
2492 os.remove(importactual)
2493 pname = self.projectdir + '/' + newname
2494 importd = pname + '/' + archiveimportdir # global: archiveimportdir
2495 os.makedirs(importd, exist_ok=True)
2496 # Dirnames to embed a VISIBLE date (UTC) of when populated.
2497 # TODO: improve dir naming or better way to store & understand later when it was processed (a log?),
2498 # without relying on file-system mtime.
2499 archived = tempfile.mkdtemp( dir=importd, prefix='{:%Y-%m-%d.%H:%M:%S}-'.format(datetime.datetime.utcnow()))
2500 tarname = self.json2targz(importfile)
2501 if tarname:
2502 with tarfile.open(tarname, mode='r:gz') as archive:
2503 for member in archive:
2504 archive.extract(member, archived)
2505 self.moveJsonPlus(importfile, archived)
2506 else:
2507 self.removeJsonPlus(importfile)
2508 return 1 # Success
2509
2510 #------------------------------------------------------------------------
2511 # Prepare multiline "warning" indicating which files to install already exist.
2512 # TODO: ugly, don't use a simple confirmation dialogue: present a proper table.
2513 #------------------------------------------------------------------------
2514 def installsConfirmMarkOverwrite(self, module, files):
2515 warning = [ "For import of module: %s," % module ]
2516 anyExists = False
2517 for i in files:
2518 exists = os.path.isfile(os.path.expanduser(i))
2519 if exists: anyExists = True
2520 warning += [ (" * " if exists else " ") + i ]
2521 if anyExists:
2522 titleSuffix = "\nCONFIRM installation of (*: OVERWRITE existing):"
2523 else:
2524 titleSuffix = "\nCONFIRM installation of:"
2525 warning[0] += titleSuffix
2526 return ConfirmInstallDialog(self, "\n".join(warning)).result
2527
2528 def vgl_install(self, importfile, pname, elecLib, newfile, isnew=True):
2529 #--------------------------------------------------------------------
2530 # Convert the in .v to: spi, cdl, elec-icon, elec-text-view forms.
2531 # TODO: Prompt to confirm final install of 5 files in dir-structure.
2532 #
2533 # newfile: argument is not used. What is it for?
2534 # Target project AND electricLib MAY BE same (pname) or different.
2535 # Rest of the filenames are determined by the module name in the source .v.
2536 #--------------------------------------------------------------------
2537
2538 newproject = self.projectdir + '/' + pname
2539 try:
2540 p = subprocess.run(['/ef/apps/bin/vglImport', importfile, pname, elecLib],
2541 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2542 check=True, universal_newlines=True)
2543 except subprocess.CalledProcessError as e:
2544 if hasattr(e, 'stdout') and e.stdout: print(e.stdout)
2545 if hasattr(e, 'stderr') and e.stderr: print(e.stderr)
2546 print('Error running vglImport: ' + str(e))
2547 if isnew == True: shutil.rmtree(newproject)
2548 return None
2549 else:
2550 dataLines = p.stdout.splitlines()
2551 if p.stderr:
2552 # Print error messages to console
2553 for i in p.stderr.splitlines(): print(i)
2554 if not dataLines or len(dataLines) != 11:
2555 print('Error: vglImport has no output, or wrong #outputs (%d vs 11)' % len(dataLines))
2556 if isnew == True: shutil.rmtree(newproject)
2557 return None
2558 else:
2559 module = dataLines[0]
2560 confirm = self.installsConfirmMarkOverwrite(module, dataLines[2::2])
2561 if not confirm:
2562 print("Cancelled")
2563 if isnew == True: shutil.rmtree(newproject)
2564 return 0
2565 # print("Proceed")
2566 clean = dataLines[1:]
2567 nbr = len(dataLines)
2568 ndx = 1
2569 # trap I/O errors and clean-up if any
2570 try:
2571 while ndx+1 < nbr:
2572 trg = os.path.expanduser(dataLines[ndx+1])
2573 os.makedirs(os.path.dirname(trg), exist_ok=True)
2574 shutil.move(dataLines[ndx], trg)
2575 ndx += 2
2576 except IOError as e:
2577 print('Error copying files: ' + str(e))
2578 for i in clean:
2579 with contextlib.suppress(FileNotFoundError): os.remove(i)
2580 if isnew == True: shutil.rmtree(newproject)
2581 return 0
2582 print( "For import of module %s installed: %s" % (module, " ".join(dataLines[2::2])))
2583 return 1 # Success
2584
2585
2586 #------------------------------------------------------------------------
2587 # Callback function from "Import Into" button on imports list box.
2588 #------------------------------------------------------------------------
2589
2590 def importintodesign(self, value):
2591 if not value['values']:
2592 print('No import selected.')
2593 return
2594
2595 # Stop the watchdog timer while this is going on
2596 self.watchclock.stop()
2597 newname = value['text']
2598 importfile = value['values'][0]
2599 print('Import project name: ' + newname + '')
2600 print('Import file name: ' + importfile + '')
2601
2602 # Behavior depends on what kind of file is being imported.
2603 # Tarballs are entire projects. Other files are individual
2604 # files and may be imported into new or existing projects
2605
2606 if os.path.isdir(importfile):
2607 print('File is a project, must import as new project.')
2608 result = self.import2project(importfile, addWarn='Redirected: A projectDir must Import-As new project.')
2609 else:
2610 ext = os.path.splitext(importfile)[1]
2611 vFile, jName, importnode = self.jsonTarVglImportable(importfile)
2612 if ((ext == '.json' and vFile) or ext == '.v'):
2613 result = self.importvglinto(newname, importfile)
2614 elif ext == '.json':
2615 # Same behavior as "Import As", at least for now
2616 print('File is a project, must import as new project.')
2617 result = self.importjson(newname, importfile)
2618 else:
2619 result = self.importspiceinto(newname, importfile)
2620
2621 if result:
2622 self.update_project_views(force=True)
2623 self.watchclock.restart()
2624
2625 #------------------------------------------------------------------------
2626 # Callback function from "Import As" button on imports list box.
2627 #------------------------------------------------------------------------
2628
2629 def importdesign(self, value):
2630 if not value['values']:
2631 print('No import selected.')
2632 return
2633
2634 # Stop the watchdog timer while this is going on
2635 self.watchclock.stop()
2636 newname = value['text']
2637 importfile = value['values'][0]
2638 print('Import project name: ' + newname)
2639 print('Import file name: ' + importfile)
2640
2641 # Behavior depends on what kind of file is being imported.
2642 # Tarballs are entire projects. Other files are individual
2643 # files and may be imported into new or existing projects
2644
2645 if os.path.isdir(importfile):
2646 result = self.import2project(importfile)
2647 else:
2648 pathext = os.path.splitext(importfile)
2649 vfile, seedname, importnode = self.jsonTarVglImportable(importfile)
2650 if ((pathext[1] == '.json' and seedname) or pathext[1] == '.v'):
2651 result = self.importvglas(newname, importfile, seedname)
2652 elif pathext[1] == '.json':
2653 result = self.importjson(newname, importfile)
2654 else:
2655 result = self.importspice(newname, importfile)
2656
2657 if result:
2658 self.update_project_views(force=True)
2659 self.watchclock.restart()
2660
2661 def deleteimport(self, value):
2662 if not value['values']:
2663 print('No import selected.')
2664 return
2665
2666 print("Delete import " + value['text'] + ' ' + value['values'][0] + " !")
2667 # Require confirmation
2668 warning = 'Confirm delete import ' + value['text'] + '?'
2669 confirm = ProtectedConfirmDialog(self, warning).result
2670 if not confirm == 'okay':
2671 return
2672 print('Delete confirmed!')
2673 item = value['values'][0]
2674
2675 if not os.path.islink(item) and os.path.isdir(item):
2676 shutil.rmtree(item)
2677 return
2678
2679 os.remove(item)
2680 ext = os.path.splitext(item)
2681 # Where import is a pair of .json and .tar.gz files, remove both.
2682 if ext[1] == '.json':
2683 if os.path.exists(ext[0] + '.tar.gz'):
2684 os.remove(ext[0] + '.tar.gz')
2685 elif os.path.exists(ext[0] + '.tgz'):
2686 os.remove(ext[0] + '.tgz')
2687
2688 def update_project_views(self, force=False):
2689 # More than updating project views, this updates projects, imports, and
2690 # IP libraries.
2691
2692 projectlist = self.get_project_list()
2693 self.projectselect.repopulate(projectlist)
2694 pdklist = self.get_pdk_list(projectlist)
2695 self.projectselect.populate2("PDK", projectlist, pdklist)
2696
2697 old_imports = self.number_of_imports
2698 importlist = self.get_import_list()
2699 self.importselect.repopulate(importlist)
2700 valuelist = self.importselect.getvaluelist()
2701 datelist = self.get_date_list(valuelist)
2702 itemlist = self.importselect.getlist()
2703 self.importselect.populate2("date", itemlist, datelist)
2704
2705 # To do: Check if itemlist in imports changed, and open if a new import
2706 # has arrived.
2707
2708 if force or (old_imports != None) and (old_imports < self.number_of_imports):
2709 self.import_open()
2710
2711 iplist = self.get_library_list()
2712 self.ipselect.repopulate(iplist, versioning=True)
2713 valuelist = self.ipselect.getvaluelist()
2714 datelist = self.get_date_list(valuelist)
2715 itemlist = self.ipselect.getlist()
2716 self.ipselect.populate2("date", itemlist, datelist)
2717
2718 def update_alert(self):
2719 # Project manager has been updated. Generate an alert window and
2720 # provide option to restart the project manager.
2721
2722 warning = 'Project manager app has been updated. Restart now?'
2723 confirm = ConfirmDialog(self, warning).result
2724 if not confirm == 'okay':
2725 print('Warning: Must quit and restart to get any fixes or updates.')
2726 return
2727 os.execl('/ef/efabless/opengalaxy/og_gui_manager.py', 'appsel_zenity.sh')
2728 # Does not return; replaces existing process.
2729
2730 #----------------------------------------------------------------------
2731 # Delete a project from the design folder.
2732 #----------------------------------------------------------------------
2733
2734 def deleteproject(self, value):
2735 if not value['values']:
2736 print('No project selected.')
2737 return
2738 print('Delete project ' + value['values'][0])
2739 # Require confirmation
2740 warning = 'Confirm delete entire project ' + value['text'] + '?'
2741 confirm = ProtectedConfirmDialog(self, warning).result
2742 if not confirm == 'okay':
2743 return
2744 shutil.rmtree(value['values'][0])
2745
2746 #----------------------------------------------------------------------
2747 # Clean out the simulation folder. Traditionally this was named
2748 # 'ngspice', so this is checked for backward-compatibility. The
2749 # proper name of the simulation directory is 'simulation'.
2750 #----------------------------------------------------------------------
2751
2752 def cleanproject(self, value):
2753 if not value['values']:
2754 print('No project selected.')
2755 return
2756 ppath = value['values'][0]
2757 print('Clean simulation raw data from directory ' + ppath)
2758 # Require confirmation
2759 warning = 'Confirm clean project ' + value['text'] + ' contents?'
2760 confirm = ConfirmDialog(self, warning).result
2761 if not confirm == 'okay':
2762 return
2763 if os.path.isdir(ppath + '/simulation'):
2764 simpath = 'simulation'
2765 elif os.path.isdir(ppath + '/ngspice'):
2766 simpath = 'ngspice'
2767 else:
2768 print('Project has no simulation folder.')
2769 return
2770
2771 filelist = os.listdir(ppath + '/' + simpath)
2772 for sfile in filelist:
2773 if os.path.splitext(sfile)[1] == '.raw':
2774 os.remove(ppath + '/ngspice/' + sfile)
2775 print('Project simulation folder cleaned.')
2776
2777 # Also clean the log file
2778 filelist = os.listdir(ppath)
2779 for sfile in filelist:
2780 if os.path.splitext(sfile)[1] == '.log':
2781 os.remove(ppath + '/' + sfile)
2782
2783 #---------------------------------------------------------------------------------------
2784 # Determine which schematic editors are compatible with the PDK, and return a list of them.
2785 #---------------------------------------------------------------------------------------
2786
2787 def list_valid_schematic_editors(self, pdktechdir):
2788 # Check PDK technology directory for xcircuit, xschem, and electric
2789 applist = []
2790 if os.path.exists(pdktechdir + '/elec'):
2791 applist.append('electric')
2792 if os.path.exists(pdktechdir + '/xschem'):
2793 applist.append('xschem')
2794 if os.path.exists(pdktechdir + '/xcircuit'):
2795 applist.append('xcircuit')
2796
2797 return applist
2798
2799 #------------------------------------------------------------------------------------------
2800 # Determine which layout editors are compatible with the PDK, and return a list of them.
2801 #------------------------------------------------------------------------------------------
2802
2803 def list_valid_layout_editors(self, pdktechdir):
2804 # Check PDK technology directory for magic and klayout
2805 applist = []
2806 if os.path.exists(pdktechdir + '/magic'):
2807 applist.append('magic')
2808 if os.path.exists(pdktechdir + '/klayout'):
2809 applist.append('klayout')
2810 return applist
2811
2812 #----------------------------------------------------------------------
2813 # Create a new project folder and initialize it (see below for steps)
2814 #----------------------------------------------------------------------
2815
2816 def createproject(self, value, seedname=None, importnode=None):
2817 # Note: value is current selection, if any, and is ignored
2818 # Require new project location and confirmation
2819 badrex1 = re.compile("^\.")
2820 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
2821 warning = 'Create new project:'
2822 print(warning)
2823 development = self.prefs['development']
2824 while True:
2825 try:
2826 if seedname:
2827 newname, newpdk = NewProjectDialog(self, warning, seed=seedname, importnode=importnode, development=development).result
2828 else:
2829 newname, newpdk = NewProjectDialog(self, warning, seed='', importnode=importnode, development=development).result
2830 except TypeError:
2831 # TypeError occurs when "Cancel" is pressed, just handle exception.
2832 return None
2833 if not newname:
2834 return None # Canceled, no action.
2835
2836 newproject = self.projectdir + '/' + newname
2837 if self.blacklisted(newname):
2838 warning = newname + ' is not allowed for a project name.'
2839 elif badrex1.match(newname):
2840 warning = 'project name may not start with "."'
2841 elif badrex2.match(newname):
2842 warning = 'project name contains illegal characters or whitespace.'
2843 elif os.path.exists(newproject):
2844 warning = newname + ' is already a project name.'
2845 else:
2846 break
2847
2848 try:
2849
2850 subprocess.Popen([og_config.apps_path + '/create_project.py', newproject, newpdk])
2851
2852 except IOError as e:
2853 print('Error copying files: ' + str(e))
2854 return None
2855
2856 except:
2857 print('Error making project.')
2858 return None
2859
2860 return newname
2861 '''
2862 # Find what tools are compatible with the given PDK
2863 schemapps = self.list_valid_schematic_editors(newpdk + '/libs.tech')
2864 layoutapps = self.list_valid_layout_editors(newpdk + '/libs.tech')
2865
2866 print('New project name will be ' + newname + '.')
2867 print('Associated project PDK is ' + newpdk + '.')
2868 try:
2869 os.makedirs(newproject)
2870
2871 # Make standard folders
2872 if 'magic' in layoutapps:
2873 os.makedirs(newproject + '/mag')
2874
2875 os.makedirs(newproject + '/spi')
2876 os.makedirs(newproject + '/spi/pex')
2877 os.makedirs(newproject + '/spi/lvs')
2878 if 'electric' in layoutapps or 'electric' in schemapps:
2879 os.makedirs(newproject + '/elec')
2880 if 'xcircuit' in schemapps:
2881 os.makedirs(newproject + '/xcirc')
2882 if 'klayout' in schemapps:
2883 os.makedirs(newproject + '/klayout')
2884 os.makedirs(newproject + '/ngspice')
2885 os.makedirs(newproject + '/ngspice/run')
2886 if 'electric' in schemapps:
2887 os.makedirs(newproject + '/ngspice/run/.allwaves')
2888 os.makedirs(newproject + '/testbench')
2889 os.makedirs(newproject + '/verilog')
2890 os.makedirs(newproject + '/verilog/source')
2891 os.makedirs(newproject + '/.ef-config')
2892 if 'xschem' in schemapps:
2893 os.makedirs(newproject + '/xschem')
2894
2895 pdkname = os.path.split(newpdk)[1]
2896
2897 # Symbolic links
2898 os.symlink(newpdk, newproject + '/.ef-config/techdir')
2899
2900 # Copy preferences
2901 # deskel = '/ef/efabless/deskel'
2902 #
2903 # Copy examples (disabled; this is too confusing to the end user. Also, they
2904 # should not be in user space at all, as they are not user editable.
2905 #
2906 # for item in os.listdir(deskel + '/exlibs'):
2907 # shutil.copytree(deskel + '/exlibs/' + item, newproject + '/elec/' + item)
2908 # for item in os.listdir(deskel + '/exmag'):
2909 # if os.path.splitext(item)[1] == '.mag':
2910 # shutil.copy(deskel + '/exmag/' + item, newproject + '/mag/' + item)
2911
2912 # Put tool-specific startup files into the appropriate user directories.
2913 if 'electric' in layoutapps or 'electric' in schemapps:
2914 self.reinitElec(newproject) # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any
2915 # Set up electric
2916 self.create_electric_header_file(newproject, newname)
2917
2918 if 'magic' in layoutapps:
2919 shutil.copy(newpdk + '/libs.tech/magic/current/' + pdkname + '.magicrc', newproject + '/mag/.magicrc')
2920
2921 if 'xcircuit' in schemapps:
2922 xcircrc = newpdk + '/libs.tech/xcircuit/' + pdkname + '.' + 'xcircuitrc'
2923 xcircrc2 = newpdk + '/libs.tech/xcircuit/xcircuitrc'
2924 if os.path.exists(xcircrc):
2925 shutil.copy(xcircrc, newproject + '/xcirc/.xcircuitrc')
2926 elif os.path.exists(xcircrc2):
2927 shutil.copy(xcircrc2, newproject + '/xcirc/.xcircuitrc')
2928
2929 if 'xschem' in schemapps:
2930 xschemrc = newpdk + '/libs.tech/xschem/xschemrc'
2931 if os.path.exists(xschemrc):
2932 shutil.copy(xschemrc, newproject + '/xschem/xschemrc')
2933
2934 except IOError as e:
2935 print('Error copying files: ' + str(e))
2936 return None
2937
2938 return newname
2939 '''
2940 #----------------------------------------------------------------------
2941 # Import a CloudV project from ~/cloudv/<project_name>
2942 #----------------------------------------------------------------------
2943
2944 def cloudvimport(self, value):
2945
2946 # Require existing project location
2947 clist = self.get_cloudv_project_list()
2948 if not clist:
2949 return 0 # No projects to import
2950 ppath = ExistingProjectDialog(self, clist, warning="Enter name of cloudV project to import:").result
2951 if not ppath:
2952 return 0 # Canceled in dialog, no action.
2953 pname = os.path.split(ppath)[1]
2954 print("Importing CloudV project " + pname)
2955
2956 importnode = None
2957 stdcell = None
2958 netlistfile = None
2959
2960 # Pull process and standard cell library from the YAML file created by
2961 # CloudV. NOTE: YAML file has multiple documents, so must use
2962 # yaml.load_all(), not yaml.load(). If there are refinements of this
2963 # process for individual build files, they will override (see further down).
2964
2965 # To do: Check entries for SoC builds. If there are multiple SoC builds,
2966 # then create an additional drop-down selection to choose one, since only
2967 # one SoC build can exist as a single Open Galaxy project. Get the name
2968 # of the top-level module for the SoC. (NOTE: It may not be intended
2969 # that there can be multiple SoC builds in the project, so for now retaining
2970 # the existing parsing assuming default names.)
2971
2972 if os.path.exists(ppath + '/.ef-config/meta.yaml'):
2973 print("Reading YAML file:")
2974 ydicts = []
2975 with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
2976 yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
2977 for ydict in yalldata:
2978 ydicts.append(ydict)
2979
2980 for ydict in ydicts:
2981 for yentry in ydict.values():
2982 if 'process' in yentry:
2983 importnode = yentry['process']
2984
2985 # If there is a file ().soc and a directory ().model, then pull the file
2986 # ().model/().model.v, which is a chip top-level netlist.
2987
2988 ydicts = []
2989 has_soc = False
2990 save_vdir = None
2991 vdirs = glob.glob(ppath + '/*')
2992 for vdir in vdirs:
2993 vnameparts = vdir.split('.')
2994 if len(vnameparts) > 1 and vnameparts[-1] == 'soc' and os.path.isdir(vdir):
2995 has_soc = True
2996 if len(vnameparts) > 1 and vnameparts[-1] == 'model':
2997 save_vdir = vdir
2998
2999 if has_soc:
3000 if save_vdir:
3001 vdir = save_vdir
3002 print("INFO: CloudV project " + vdir + " is a full chip SoC project.")
3003
3004 vroot = os.path.split(vdir)[1]
3005 netlistfile = vdir + '/' + vroot + '.v'
3006 if os.path.exists(netlistfile):
3007 print("INFO: CloudV chip top level verilog is " + netlistfile + ".")
3008 else:
3009 print("ERROR: Expected SoC .model directory not found.")
3010
3011 # Otherwise, if the project has a build/ directory and a netlist.v file,
3012 # then set the foundry node accordingly.
3013
3014 elif os.path.exists(ppath + '/build'):
3015 vfiles = glob.glob(ppath + '/build/*.v')
3016 for vfile in vfiles:
3017 vroot = os.path.splitext(vfile)[0]
3018 if os.path.splitext(vroot)[1] == '.netlist':
3019 netlistfile = ppath + '/build/' + vfile
3020
3021 # Pull process and standard cell library from the YAML file
3022 # created by CloudV
3023 # Use yaml.load_all(), not yaml.load() (see above)
3024
3025 if os.path.exists(ppath + '/.ef-config/meta.yaml'):
3026 print("Reading YAML file:")
3027 ydicts = []
3028 with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
3029 yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
3030 for ydict in yalldata:
3031 ydicts.append(ydict)
3032
3033 for ydict in ydicts:
3034 for yentry in ydict.values():
3035 if 'process' in yentry:
3036 importnode = yentry['process']
3037 if 'stdcell' in yentry:
3038 stdcell = yentry['stdcell']
3039 break
3040
3041 if importnode:
3042 print("INFO: Project targets foundry process " + importnode + ".")
3043 else:
3044 print("WARNING: Project does not target any foundry process.")
3045
3046 newname = self.createproject(value, seedname=pname, importnode=importnode)
3047 if not newname: return 0 # Canceled in dialog, no action.
3048 newpath = self.projectdir + '/' + newname
3049
3050 result = self.install_from_cloudv(ppath, newpath, importnode, stdcell, ydicts)
3051 if result == None:
3052 print('Error during import.')
3053 return None
3054 elif result == 0:
3055 return 0 # Canceled
3056 else:
3057 return 1 # Success
3058
3059 #----------------------------------------------------------------------
3060 # Make a copy of a project in the design folder.
3061 #----------------------------------------------------------------------
3062
3063 def copyproject(self, value):
3064 if not value['values']:
3065 print('No project selected.')
3066 return
3067 # Require copy-to location and confirmation
3068 badrex1 = re.compile("^\.")
3069 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3070 warning = 'Copy project ' + value['text'] + ' to new project.'
3071 print('Copy project directory ' + value['values'][0])
3072 newname = ''
3073 copylist = []
3074 elprefs = False
3075 spprefs = False
3076 while True:
3077 copylist = CopyProjectDialog(self, warning, seed=newname).result
3078 if not copylist:
3079 return # Canceled, no action.
3080 else:
3081 newname = copylist[0]
3082 elprefs = copylist[1]
3083 spprefs = copylist[2]
3084 newproject = self.projectdir + '/' + newname
3085 if self.blacklisted(newname):
3086 warning = newname + ' is not allowed for a project name.'
3087 elif badrex1.match(newname):
3088 warning = 'project name may not start with "."'
3089 elif badrex2.match(newname):
3090 warning = 'project name contains illegal characters or whitespace.'
3091 elif os.path.exists(newproject):
3092 warning = newname + ' is already a project name.'
3093 else:
3094 break
3095
3096 oldpath = value['values'][0]
3097 oldname = os.path.split(oldpath)[1]
3098 patterns = [oldname + '.log']
3099 if not elprefs:
3100 patterns.append('.java')
3101 if not spprefs:
3102 patterns.append('ngspice')
3103 patterns.append('pv')
3104
3105 print("New project name will be " + newname)
3106 try:
3107 shutil.copytree(oldpath, newproject, symlinks = True,
3108 ignore = shutil.ignore_patterns(*patterns))
3109 except IOError as e:
3110 print('Error copying files: ' + str(e))
3111 return
3112
3113 # NOTE: Behavior is for project files to depend on "ip-name". Using
3114 # the project filename as a project name is a fallback behavior. If
3115 # there is a project.json file, and it defines an ip-name entry, then
3116 # there is no need to make changes within the project. If there is
3117 # no project.json file, then create one and set the ip-name entry to
3118 # the old project name, which avoids the need to make changes within
3119 # the project.
3120
3121 else:
3122 # Check project.json
3123 jsonname = newproject + '/project.json'
3124 legacyname = newproject + '/' + oldname + '.json'
3125 if not os.path.isfile(jsonname):
3126 if os.path.isfile(legacyname):
3127 jsonname = legacyname
3128
3129 found = False
3130 if os.path.isfile(jsonname):
3131 # Pull the ipname into local store (may want to do this with the
3132 # datasheet as well)
3133 with open(jsonname, 'r') as f:
3134 datatop = json.load(f)
3135 dsheet = datatop['data-sheet']
3136 if 'ip-name' in dsheet:
3137 found = True
3138
3139 if not found:
3140 jData = self.create_ad_hoc_json(oldname, newproject)
3141 with open(newproject + '/project.json', 'w') as ofile:
3142 json.dump(jData, ofile, indent = 4)
3143
3144 # If ngspice and electric prefs were not copied from the source
3145 # to the target, as recommended, then copy these from the
3146 # skeleton repository as is done when creating a new project.
3147
3148 if not spprefs:
3149 try:
3150 os.makedirs(newproject + '/ngspice')
3151 os.makedirs(newproject + '/ngspice/run')
3152 os.makedirs(newproject + '/ngspice/run/.allwaves')
3153 except FileExistsError:
3154 pass
3155 if not elprefs:
3156 # Copy preferences
3157 deskel = '/ef/efabless/deskel'
3158 try:
3159 shutil.copytree(deskel + '/dotjava', newproject + '/elec/.java', symlinks = True)
3160 except IOError as e:
3161 print('Error copying files: ' + e)
3162
3163 #----------------------------------------------------------------------
3164 # Change a project IP to a different name.
3165 #----------------------------------------------------------------------
3166
3167 def renameproject(self, value):
3168 if not value['values']:
3169 print('No project selected.')
3170 return
3171
3172 # Require new project name and confirmation
3173 badrex1 = re.compile("^\.")
3174 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3175 projname = value['text']
3176
3177 # Find the IP name for project projname. If it has a JSON file, then
3178 # read it and pull the ip-name record. If not, the fallback position
3179 # is to assume that the project filename is the project name.
3180
3181 # Check project.json
3182 projectpath = self.projectdir + '/' + projname
3183 jsonname = projectpath + '/project.json'
3184 legacyname = projectpath + '/' + projname + '.json'
3185 if not os.path.isfile(jsonname):
3186 if os.path.isfile(legacyname):
3187 jsonname = legacyname
3188
3189 oldname = projname
3190 if os.path.isfile(jsonname):
3191 # Pull the ipname into local store (may want to do this with the
3192 # datasheet as well)
3193 with open(jsonname, 'r') as f:
3194 datatop = json.load(f)
3195 dsheet = datatop['data-sheet']
3196 if 'ip-name' in dsheet:
3197 oldname = dsheet['ip-name']
3198
3199 warning = 'Rename IP "' + oldname + '" for project ' + projname + ':'
3200 print(warning)
3201 newname = projname
3202 while True:
3203 try:
3204 newname = ProjectNameDialog(self, warning, seed=oldname + '_1').result
3205 except TypeError:
3206 # TypeError occurs when "Cancel" is pressed, just handle exception.
3207 return None
3208 if not newname:
3209 return None # Canceled, no action.
3210
3211 if self.blacklisted(newname):
3212 warning = newname + ' is not allowed for an IP name.'
3213 elif badrex1.match(newname):
3214 warning = 'IP name may not start with "."'
3215 elif badrex2.match(newname):
3216 warning = 'IP name contains illegal characters or whitespace.'
3217 else:
3218 break
3219
3220 # Update everything, including schematic, symbol, layout, JSON file, etc.
3221 print('New project IP name will be ' + newname + '.')
3222 rename_project_all(projectpath, newname)
3223
3224 # class vars: one-time compile of regulare expressions for life of the process
3225 projNameBadrex1 = re.compile("^[-.]")
3226 projNameBadrex2 = re.compile(".*[][{}()!/ \t\n\\\><#$\*\?\"'|`~]")
3227 importProjNameBadrex1 = re.compile(".*[.]bak$")
3228
3229 # centralize legal projectName check.
3230 # TODO: Several code sections are not yet converted to use this.
3231 # TODO: Extend to explain to the user the reason why.
3232 def validProjectName(self, name):
3233 return not (self.blacklisted(name) or
3234 self.projNameBadrex1.match(name) or
3235 self.projNameBadrex2.match(name))
3236
3237 #----------------------------------------------------------------------
3238 # "Import As" a dir in import/ as a project. based on renameproject().
3239 # addWarn is used to augment confirm-dialogue if redirected here via erroneous ImportInto
3240 #----------------------------------------------------------------------
3241
3242 def import2project(self, importfile, addWarn=None):
3243 name = os.path.split(importfile)[1]
3244 projpath = self.projectdir + '/' + name
3245
3246 bakname = name + '.bak'
3247 bakpath = self.projectdir + '/' + bakname
3248 warns = []
3249 if addWarn:
3250 warns += [ addWarn ]
3251
3252 # Require new project name and confirmation
3253 confirmPrompt = None # use default: I am sure I want to do this.
3254 if os.path.isdir(projpath):
3255 if warns:
3256 warns += [ '' ] # blank line between addWarn and below two Warnings:
3257 if os.path.isdir(bakpath):
3258 warns += [ 'Warning: Replacing EXISTING: ' + name + ' AND ' + bakname + '!' ]
3259 else:
3260 warns += [ 'Warning: Replacing EXISTING: ' + name + '!' ]
3261 warns += [ 'Warning: Check for & exit any Electric,magic,qflow... for above project(s)!\n' ]
3262 confirmPrompt = 'I checked & exited apps and am sure I want to do this.'
3263
3264 warns += [ 'Confirm import-as new project: ' + name + '?' ]
3265 warning = '\n'.join(warns)
3266 confirm = ProtectedConfirmDialog(self, warning, confirmPrompt=confirmPrompt).result
3267 if not confirm == 'okay':
3268 return
3269
3270 print('New project name will be ' + name + '.')
3271 try:
3272 if os.path.isdir(projpath):
3273 if os.path.isdir(bakpath):
3274 print('Deleting old project: ' + bakpath);
3275 shutil.rmtree(bakpath)
3276 print('Moving old project ' + name + ' to ' + bakname)
3277 os.rename( projpath, bakpath)
3278 print("Importing as new project " + name)
3279 os.rename(importfile, projpath)
3280 return True
3281 except IOError as e:
3282 print("Error importing-as project: " + str(e))
3283 return None
3284
3285 #----------------------------------------------------------------------
3286 # Helper subroutine:
3287 # Check if a project is a valid project. Return the name of the
3288 # datasheet if the project has a valid one in the project top level
3289 # path.
3290 #----------------------------------------------------------------------
3291
3292 def get_datasheet_name(self, dpath):
3293 if not os.path.isdir(dpath):
3294 print('Error: Project is not a folder!')
3295 return
3296 # Check for valid datasheet name in the following order:
3297 # (1) project.json (Legacy)
3298 # (2) <name of directory>.json (Legacy)
3299 # (3) not "datasheet.json" or "datasheet_anno.json"
3300 # (4) "datasheet.json"
3301 # (5) "datasheet_anno.json"
3302
3303 dsname = os.path.split(dpath)[1]
3304 if os.path.isfile(dpath + '/project.json'):
3305 datasheet = dpath + '/project.json'
3306 elif os.path.isfile(dpath + '/' + dsname + '.json'):
3307 datasheet = dpath + '/' + dsname + '.json'
3308 else:
3309 has_generic = False
3310 has_generic_anno = False
3311 filelist = os.listdir(dpath)
3312 for file in filelist[:]:
3313 if os.path.splitext(file)[1] != '.json':
3314 filelist.remove(file)
3315 if 'datasheet.json' in filelist:
3316 has_generic = True
3317 filelist.remove('datasheet.json')
3318 if 'datasheet_anno.json' in filelist:
3319 has_generic_anno = True
3320 filelist.remove('datasheet_anno.json')
3321 if len(filelist) == 1:
3322 print('Trying ' + dpath + '/' + filelist[0])
3323 datasheet = dpath + '/' + filelist[0]
3324 elif has_generic:
3325 datasheet + dpath + '/datasheet.json'
3326 elif has_generic_anno:
3327 datasheet + dpath + '/datasheet_anno.json'
3328 else:
3329 if len(filelist) > 1:
3330 print('Error: Path ' + dpath + ' has ' + str(len(filelist)) +
3331 ' valid datasheets.')
3332 else:
3333 print('Error: Path ' + dpath + ' has no valid datasheets.')
3334 return None
3335
3336 if not os.path.isfile(datasheet):
3337 print('Error: File ' + datasheet + ' not found.')
3338 return None
3339 else:
3340 return datasheet
3341
3342 #----------------------------------------------------------------------
3343 # Run the LVS manager
3344 #----------------------------------------------------------------------
3345
3346 def run_lvs(self):
3347 value = self.projectselect.selected()
3348 if value:
3349 design = value['values'][0]
3350 # designname = value['text']
3351 designname = self.project_name
3352 print('Run LVS on design ' + designname + ' (' + design + ')')
3353 # use Popen, not run, so that application does not wait for it to exit.
3354 subprocess.Popen([og_config.apps_path + '/lvs_manager.py', design, designname])
3355 else:
3356 print("You must first select a project.", file=sys.stderr)
3357
3358 #----------------------------------------------------------------------
3359 # Run the local characterization checker
3360 #----------------------------------------------------------------------
3361
3362 def characterize(self):
3363 value = self.projectselect.selected()
3364 if value:
3365 design = value['values'][0]
3366 # designname = value['text']
3367 designname = self.project_name
3368 datasheet = self.get_datasheet_name(design)
3369 print('Characterize design ' + designname + ' (' + datasheet + ' )')
3370 if datasheet:
3371 # use Popen, not run, so that application does not wait for it to exit.
3372 dsheetroot = os.path.splitext(datasheet)[0]
3373 subprocess.Popen([og_config.apps_path + '/og_gui_characterize.py',
3374 datasheet])
3375 else:
3376 print("You must first select a project.", file=sys.stderr)
3377
3378 #----------------------------------------------------------------------
3379 # Run the local synthesis tool (qflow)
3380 #----------------------------------------------------------------------
3381
3382 def synthesize(self):
3383 value = self.projectselect.selected()
3384 if value:
3385 design = value['values'][0]
3386 # designname = value['text']
3387 designname = self.project_name
3388 development = self.prefs['devstdcells']
3389 if not designname:
3390 # A project without a datasheet has no designname (which comes from
3391 # the 'ip-name' record in the datasheet JSON) but can still be
3392 # synthesized.
3393 designname = design
3394
3395 # Normally there is one digital design in a project. However, full-chip
3396 # designs (in particular) may have multiple sub-projects that are
3397 # independently synthesized digital blocks. Find all subdirectories of
3398 # the top level or subdirectories of qflow that contain a 'qflow_vars.sh'
3399 # file. If there is more than one, then present a list. If there is
3400 # only one but it is not in 'qflow/', then be sure to pass the actual
3401 # directory name to the qflow manager.
3402
3403 qvlist = glob.glob(design + '/*/qflow_vars.sh')
3404 qvlist.extend(glob.glob(design + '/qflow/*/qflow_vars.sh'))
3405 if len(qvlist) > 1 or (len(qvlist) == 1 and not os.path.exists(design + '/qflow/qflow_vars.sh')):
3406 # Generate selection menu
3407 if len(qvlist) > 1:
3408 clist = list(os.path.split(item)[0] for item in qvlist)
3409 ppath = ExistingProjectDialog(self, clist, warning="Enter name of qflow project to open:").result
3410 if not ppath:
3411 return 0 # Canceled in dialog, no action.
3412 else:
3413 ppath = os.path.split(qvlist[0])[0]
3414
3415 # pname is everything in ppath after matching design:
3416 pname = ppath.replace(design + '/', '')
3417
3418 print('Synthesize design in qflow project directory ' + pname)
3419 if development:
3420 subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
3421 design, '-development', '-subproject=' + pname])
3422 else:
3423 subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
3424 design, '-subproject=' + pname])
3425 else:
3426 print('Synthesize design ' + designname + ' (' + design + ')')
3427 # use Popen, not run, so that application does not wait for it to exit.
3428 if development:
3429 subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
3430 design, designname, '-development'])
3431 else:
3432 subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
3433 design, designname])
3434 else:
3435 print("You must first select a project.", file=sys.stderr)
3436
3437 #----------------------------------------------------------------------
3438 # Switch between showing and hiding the import list (default hidden)
3439 #----------------------------------------------------------------------
3440
3441 def import_toggle(self):
3442 import_state = self.toppane.import_frame.import_header3.cget('text')
3443 if import_state == '+':
3444 self.importselect.grid(row = 11, sticky = 'news')
3445 self.toppane.import_frame.import_header3.config(text='-')
3446 else:
3447 self.importselect.grid_forget()
3448 self.toppane.import_frame.import_header3.config(text='+')
3449
3450 def import_open(self):
3451 self.importselect.grid(row = 11, sticky = 'news')
3452 self.toppane.import_frame.import_header3.config(text='-')
3453
3454 #----------------------------------------------------------------------
3455 # Switch between showing and hiding the IP library list (default hidden)
3456 #----------------------------------------------------------------------
3457
3458 def library_toggle(self):
3459 library_state = self.toppane.library_frame.library_header3.cget('text')
3460 if library_state == '+':
3461 self.ipselect.grid(row = 8, sticky = 'news')
3462 self.toppane.library_frame.library_header3.config(text='-')
3463 else:
3464 self.ipselect.grid_forget()
3465 self.toppane.library_frame.library_header3.config(text='+')
3466
3467 def library_open(self):
3468 self.ipselect.grid(row = 8, sticky = 'news')
3469 self.toppane.library_frame.library_header3.config(text='-')
3470
3471 #----------------------------------------------------------------------
3472 # Run padframe-calc (today internally invokes libreoffice, we only need cwd set to design project)
3473 #----------------------------------------------------------------------
3474 def padframe_calc(self):
3475 value = self.projectselect.selected()
3476 if value:
3477 designname = self.project_name
3478 self.padframe_calc_work(newname=designname)
3479 else:
3480 print("You must first select a project.", file=sys.stderr)
3481
3482 #------------------------------------------------------------------------
3483 # Run padframe-calc (today internally invokes libreoffice, we set cwd to design project)
3484 # Modelled somewhat after 'def importvgl':
3485 # Prompt for an existing electric lib.
3486 # Prompt for a target cellname (for both mag and electric icon).
3487 # (The AS vs INTO behavior is incomplete as yet. Used so far with current-project as newname arg).
3488 # newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
3489 # Either newname is given: we PROMPT to pick an existing elecLib;
3490 # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
3491 #------------------------------------------------------------------------
3492 def padframe_calc_work(self, newname=None):
3493 elecLib = newname
3494 isnew = not newname
3495 if isnew:
3496 # Use create project code first to generate a valid project space.
3497 newname = self.createproject(None)
3498 if not newname: return 0 # Canceled in dialog, no action.
3499 # print("padframe-calc in new project " + newname + ".")
3500 elecLib = newname
3501
3502 # For life of this projectManager process, store/recall last PadFrame Settings per project
3503 global project2pfd
3504 try:
3505 project2pfd
3506 except:
3507 project2pfd = {}
3508 if newname not in project2pfd:
3509 project2pfd[newname] = {"libEntry": None, "cellName": None}
3510
3511 ppath = self.projectdir + '/' + newname
3512 choices = self.get_elecLib_list(newname)
3513 if not choices:
3514 print( "Aborted: No existing electric libraries found to write symbol into.")
3515 return 0
3516
3517 elecLib = newname + '/elec/' + elecLib + '.delib'
3518 elecLib = project2pfd[newname]["libEntry"] or elecLib
3519 cellname = project2pfd[newname]["cellName"] or "padframe"
3520 libAndCell = ExistingElecLibCellDialog(self, None, title="PadFrame Settings", plist=choices, descPost="of icon&layout", seedLibNm=elecLib, seedCellNm=cellname).result
3521 if not libAndCell:
3522 return 0 # Canceled in dialog, no action.
3523
3524 (elecLib, cellname) = libAndCell
3525 if not cellname:
3526 return 0 # empty cellname, no action.
3527
3528 project2pfd[newname]["libEntry"] = elecLib
3529 project2pfd[newname]["cellName"] = cellname
3530
3531 # Isolate just electric lib name without extension. ../a/b.delib -> b
3532 elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
3533 print("padframe-calc in project: %s, elecLib: %s, cellName: %s" % (newname, elecLib, cellname))
3534
3535 export = dict(os.environ)
3536 export['EF_DESIGNDIR'] = ppath
3537 subprocess.Popen(['/ef/apps/bin/padframe-calc', elecLib, cellname], cwd = ppath, env = export)
3538
3539 # not yet any useful return value or reporting of results here in projectManager...
3540 return 1
3541
3542 #----------------------------------------------------------------------
3543 # Run the schematic editor (tool as given by user preference)
3544 #----------------------------------------------------------------------
3545
3546 def edit_schematic(self):
3547 value = self.projectselect.selected()
3548 if value:
3549 design = value['values'][0]
3550
3551 pdktechdir = design + self.config_path(design)+'/techdir/libs.tech'
3552
3553 applist = self.list_valid_schematic_editors(pdktechdir)
3554
3555 if len(applist)==0:
3556 print("Unable to find a valid schematic editor.")
3557 return
3558
3559 # If the preferred app is in the list, then use it.
3560
3561 if self.prefs['schemeditor'] in applist:
3562 appused = self.prefs['schemeditor']
3563 else:
3564 appused = applist[0]
3565
3566 if appused == 'xcircuit':
3567 return self.edit_schematic_with_xcircuit()
3568 elif appused == 'xschem':
3569 return self.edit_schematic_with_xschem()
3570 elif appused == 'electric':
3571 return self.edit_schematic_with_electric()
3572 else:
3573 print("Unknown/unsupported schematic editor " + appused + ".", file=sys.stderr)
3574
3575 else:
3576 print("You must first select a project.", file=sys.stderr)
3577
3578 #----------------------------------------------------------------------
3579 # Run the schematic editor (electric)
3580 #----------------------------------------------------------------------
3581
3582 def edit_schematic_with_electric(self):
3583 value = self.projectselect.selected()
3584 if value:
3585 design = value['values'][0]
3586 # designname = value['text']
3587 # self.project_name set by setcurrent. This is the true project
3588 # name, as opposed to the directory name.
3589 designname = self.project_name
3590 print('Edit schematic ' + designname + ' (' + design + ' )')
3591 # Collect libs on command-line; electric opens these in Explorer
3592 libs = []
3593 ellibrex = re.compile(r'^(tech_.*|ef_examples)\.[dj]elib$', re.IGNORECASE)
3594
3595 self.reinitElec(design)
3596
3597 # /elec and /.java are prerequisites for running electric
3598 if not os.path.exists(design + '/elec'):
3599 print("No path to electric design folder.")
3600 return
3601
3602 if not os.path.exists(design + '/elec/.java'):
3603 print("No path to electric .java folder.")
3604 return
3605
3606 # Fix the LIBDIRS file if needed
3607 #fix_libdirs(design, create = True)
3608
3609 # Check for legacy directory (missing .ef-config and/or .ef-config/techdir);
3610 # Handle as necessary.
3611
3612 # don't sometimes yield pdkdir as some subdir of techdir
3613 pdkdir = design + self.config_path(design) + '/techdir/'
3614 if not os.path.exists(pdkdir):
3615 export = dict(os.environ)
3616 export['EF_DESIGNDIR'] = design
3617 '''
3618 p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
3619 stdout = subprocess.PIPE, env = export)
3620 config_out = p.stdout.splitlines()
3621 for line in config_out:
3622 setline = line.decode('utf-8').split('=')
3623 if setline[0] == 'EF_TECHDIR':
3624 pdkdir = re.sub("[';]", "", setline[1])
3625 '''
3626
3627 for subpath in ('libs.tech/elec/', 'libs.ref/elec/'):
3628 pdkelec = os.path.join(pdkdir, subpath)
3629 if os.path.exists(pdkelec) and os.path.isdir(pdkelec):
3630 # don't use os.walk(), it is recursive, wastes time
3631 for entry in os.scandir(pdkelec):
3632 if ellibrex.match(entry.name):
3633 libs.append(entry.path)
3634
3635 # Locate most useful project-local elec-lib to open on electric cmd-line.
3636 designroot = os.path.split(design)[1]
3637 finalInDesDirLibAdded = False
3638 if os.path.exists(design + '/elec/' + designname + '.jelib'):
3639 libs.append(design + '/elec/' + designname + '.jelib')
3640 finalInDesDirLibAdded = True
3641 elif os.path.isdir(design + '/elec/' + designname + '.delib'):
3642 libs.append(design + '/elec/' + designname + '.delib')
3643 finalInDesDirLibAdded = True
3644 else:
3645 # Alternative path is the project name + .delib
3646 if os.path.isdir(design + '/elec/' + designroot + '.delib'):
3647 libs.append(design + '/elec/' + designroot + '.delib')
3648 finalInDesDirLibAdded = True
3649
3650 # Finally, check for the one absolute requirement for a project,
3651 # which is that there must be a symbol designname + .ic in the
3652 # last directory. If not, then do a search for it.
3653 if not finalInDesDirLibAdded or not os.path.isfile(libs[-1] + '/' + designname + '.ic'):
3654 delibdirs = os.listdir(design + '/elec')
3655 for delibdir in delibdirs:
3656 if os.path.splitext(delibdir)[1] == '.delib':
3657 iconfiles = os.listdir(design + '/elec/' + delibdir)
3658 for iconfile in iconfiles:
3659 if iconfile == designname + '.ic':
3660 libs.append(design + '/elec/' + delibdir)
3661 finalInDesDirLibAdded = True
3662 break
3663
3664 # Above project-local lib-adds are all conditional on finding some lib
3665 # with an expected name or content: all of which may fail.
3666 # Force last item ALWAYS to be 'a path' in the project's elec/ dir.
3667 # Usually it's a real library (found above). (If lib does not exist the messages
3668 # window does get an error message). But the purpose is for the universal side-effect:
3669 # To EVERY TIME reseed the File/OpenLibrary dialogue WorkDir to start in
3670 # project's elec/ dir; avoid it starting somewhere in the PDK, which
3671 # is what will happen if last actual cmd-line arg is a lib in the PDK, and
3672 # about which users have complained. (Optimal fix needs electric enhancement).
3673 if not finalInDesDirLibAdded:
3674 libs.append(design + '/elec/' + designroot + '.delib')
3675
3676 # Pull last item from libs and make it a command-line argument.
3677 # All other libraries become part of the EOPENARGS environment variable,
3678 # and electric is called with the elecOpen.bsh script.
3679 indirectlibs = libs[:-1]
3680 export = dict(os.environ)
3681 arguments = []
3682 if indirectlibs:
3683 export['EOPENARGS'] = ' '.join(indirectlibs)
3684 arguments.append('-s')
3685 arguments.append('/ef/efabless/lib/elec/elecOpen.bsh')
3686
3687 try:
3688 arguments.append(libs[-1])
3689 except IndexError:
3690 print('Error: Electric project directories not set up correctly?')
3691 else:
3692 subprocess.Popen(['electric', *arguments], cwd = design + '/elec',
3693 env = export)
3694 else:
3695 print("You must first select a project.", file=sys.stderr)
3696
3697 #----------------------------------------------------------------------
3698 # Run the schematic editor (xcircuit)
3699 #----------------------------------------------------------------------
3700
3701 def edit_schematic_with_xcircuit(self):
3702 value = self.projectselect.selected()
3703 if value:
3704 design = value['values'][0]
3705 # designname = value['text']
3706 # self.project_name set by setcurrent. This is the true project
3707 # name, as opposed to the directory name.
3708 designname = self.project_name
3709 print('Edit schematic ' + designname + ' (' + design + ' )')
3710 xcircdirpath = design + '/xcirc'
3711 pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xcircuit'
3712
3713 # /xcirc directory is a prerequisite for running xcircuit. If it doesn't
3714 # exist, create it and seed it with .xcircuitrc from the tech directory
3715 if not os.path.exists(xcircdirpath):
3716 os.makedirs(xcircdirpath)
3717
3718 # Copy xcircuit startup file from tech directory
3719 hasxcircrcfile = os.path.exists(xcircdirpath + '/.xcircuitrc')
3720 if not hasxcircrcfile:
3721 if os.path.exists(pdkdir + '/xcircuitrc'):
3722 shutil.copy(pdkdir + '/xcircuitrc', xcircdirpath + '/.xcircuitrc')
3723
3724 # Command line argument is the project name
3725 arguments = [design + '/xcirc' + designname]
3726 subprocess.Popen(['xcircuit', *arguments])
3727 else:
3728 print("You must first select a project.", file=sys.stderr)
3729
3730 #----------------------------------------------------------------------
3731 # Run the schematic editor (xschem)
3732 #----------------------------------------------------------------------
3733
3734 def edit_schematic_with_xschem(self):
3735 value = self.projectselect.selected()
3736 if value:
3737 design = value['values'][0]
3738 # self.project_name set by setcurrent. This is the true project
3739 # name, as opposed to the directory name.
3740 designname = self.project_name
3741 print('Edit schematic ' + designname + ' (' + design + ' )')
3742 xschemdirpath = design + '/xschem'
3743
3744 pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xschem'
3745
3746
3747 # /xschem directory is a prerequisite for running xschem. If it doesn't
3748 # exist, create it and seed it with xschemrc from the tech directory
3749 if not os.path.exists(xschemdirpath):
3750 os.makedirs(xschemdirpath)
3751
3752 # Copy xschem startup file from tech directory
3753 hasxschemrcfile = os.path.exists(xschemdirpath + '/xschemrc')
3754 if not hasxschemrcfile:
3755 if os.path.exists(pdkdir + '/xschemrc'):
3756 shutil.copy(pdkdir + '/xschemrc', xschemdirpath + '/xschemrc')
3757
3758 # Command line argument is the project name. The "-r" option is recommended if there
3759 # is no stdin/stdout piping.
3760
3761 arguments = ['-r', design + '/xschem/' + designname]
3762 subprocess.Popen(['xschem', *arguments])
3763 else:
3764 print("You must first select a project.", file=sys.stderr)
3765
3766 #----------------------------------------------------------------------
3767 # Run the layout editor (magic or klayout)
3768 #----------------------------------------------------------------------
3769
3770 def edit_layout(self):
3771 value = self.projectselect.selected()
3772 if value:
3773 design = value['values'][0]
3774 pdktechdir = design + self.config_path(design) + '/techdir/libs.tech'
3775
3776 applist = self.list_valid_layout_editors(pdktechdir)
3777
3778 if len(applist)==0:
3779 print("Unable to find a valid layout editor.")
3780 return
3781
3782 # If the preferred app is in the list, then use it.
3783 if self.prefs['layouteditor'] in applist:
3784 appused = self.prefs['layouteditor']
3785 else:
3786 appused = applist[0]
3787
3788 if appused == 'magic':
3789 return self.edit_layout_with_magic()
3790 elif appused == 'klayout':
3791 return self.edit_layout_with_klayout()
3792 elif appused == 'electric':
3793 return self.edit_layout_with_electric()
3794 else:
3795 print("Unknown/unsupported layout editor " + appused + ".", file=sys.stderr)
3796
3797 else:
3798 print("You must first select a project.", file=sys.stderr)
3799
3800 #----------------------------------------------------------------------
3801 # Run the magic layout editor
3802 #----------------------------------------------------------------------
3803
3804 def edit_layout_with_magic(self):
3805 value = self.projectselect.selected()
3806 if value:
3807 design = value['values'][0]
3808 # designname = value['text']
3809 designname = self.project_name
3810
3811 pdkdir = ''
3812 pdkname = ''
3813
3814 if os.path.exists(design + '/.ef-config/techdir/libs.tech'):
3815 pdkdir = design + '/.ef-config/techdir/libs.tech/magic/current'
3816 pdkname = os.path.split(os.path.realpath(design + '/.ef-config/techdir'))[1]
3817 elif os.path.exists(design + '/.config/techdir/libs.tech'):
3818 pdkdir = design + '/.config/techdir/libs.tech/magic'
3819 pdkname = os.path.split(os.path.realpath(design + '/.config/techdir'))[1]
3820
3821
3822 # Check if the project has a /mag directory. Create it and
3823 # put the correct .magicrc file in it, if it doesn't.
3824 magdirpath = design + '/mag'
3825 hasmagdir = os.path.exists(magdirpath)
3826 if not hasmagdir:
3827 os.makedirs(magdirpath)
3828
3829 hasmagrcfile = os.path.exists(magdirpath + '/.magicrc')
3830 if not hasmagrcfile:
3831 shutil.copy(pdkdir + '/' + pdkname + '.magicrc', magdirpath + '/.magicrc')
3832
3833 # Check if the .mag file exists for the project. If not,
3834 # generate a dialog.
3835 magpath = design + '/mag/' + designname + '.mag'
3836 netpath = design + '/spi/' + designname + '.spi'
3837 # print("magpath is " + magpath)
3838 hasmag = os.path.exists(magpath)
3839 hasnet = os.path.exists(netpath)
3840 if hasmag:
3841 if hasnet:
3842 statbuf1 = os.stat(magpath)
3843 statbuf2 = os.stat(netpath)
3844 # No specific action for out-of-date layout. To be done:
3845 # Check contents and determine if additional devices need to
3846 # be added to the layout. This may be more trouble than it's
3847 # worth.
3848 #
3849 # if statbuf2.st_mtime > statbuf1.st_mtime:
3850 # hasmag = False
3851
3852 if not hasmag:
3853 # Does the project have any .mag files at all? If so, the project
3854 # layout may be under a name different than the project name. If
3855 # so, present the user with a selectable list of layout names,
3856 # with the option to start a new layout or import from schematic.
3857
3858 maglist = os.listdir(design + '/mag/')
3859 if len(maglist) > 1:
3860 # Generate selection menu
3861 warning = 'No layout matches IP name ' + designname + '.'
3862 maglist = list(item for item in maglist if os.path.splitext(item)[1] == '.mag')
3863 clist = list(os.path.splitext(item)[0] for item in maglist)
3864 ppath = EditLayoutDialog(self, clist, ppath=design,
3865 pname=designname, warning=warning,
3866 hasnet=hasnet).result
3867 if not ppath:
3868 return 0 # Canceled in dialog, no action.
3869 elif ppath != '(New layout)':
3870 hasmag = True
3871 designname = ppath
3872 elif len(maglist) == 1:
3873 # Only one magic file, no selection, just bring it up.
3874 designname = os.path.split(maglist[0])[1]
3875 hasmag = True
3876
3877 if not hasmag:
3878 populate = NewLayoutDialog(self, "No layout for project.").result
3879 if not populate:
3880 return 0 # Canceled, no action.
3881 elif populate():
3882 # Name of PDK deprecated. The .magicrc file in the /mag directory
3883 # will load the correct PDK and specify the proper library for the
3884 # low-level device namespace, which may not be the same as techdir.
3885 # NOTE: netlist_to_layout script will attempt to generate a
3886 # schematic netlist if one does not exist.
3887
3888 print('Running /ef/efabless/bin/netlist_to_layout.py ../spi/' + designname + '.spi')
3889 try:
3890 p = subprocess.run(['/ef/efabless/bin/netlist_to_layout.py',
3891 '../spi/' + designname + '.spi'],
3892 stdin = subprocess.PIPE, stdout = subprocess.PIPE,
3893 stderr = subprocess.PIPE, cwd = design + '/mag')
3894 if p.stderr:
3895 err_string = p.stderr.splitlines()[0].decode('utf-8')
3896 # Print error messages to console
3897 print(err_string)
3898
3899 except subprocess.CalledProcessError as e:
3900 print('Error running netlist_to_layout.py: ' + e.output.decode('utf-8'))
3901 else:
3902 if os.path.exists(design + '/mag/create_script.tcl'):
3903 with open(design + '/mag/create_script.tcl', 'r') as infile:
3904 magproc = subprocess.run(['/ef/apps/bin/magic',
3905 '-dnull', '-noconsole', '-rcfile ',
3906 pdkdir + '/' + pdkname + '.magicrc', designname],
3907 stdin = infile, stdout = subprocess.PIPE,
3908 stderr = subprocess.PIPE, cwd = design + '/mag')
3909 print("Populated layout cell")
3910 # os.remove(design + '/mag/create_script.tcl')
3911 else:
3912 print("No device generating script was created.", file=sys.stderr)
3913
3914 print('Edit layout ' + designname + ' (' + design + ' )')
3915
3916 magiccommand = ['magic']
3917 # Select the graphics package used by magic from the profile settings.
3918 if 'magic-graphics' in self.prefs:
3919 magiccommand.extend(['-d', self.prefs['magic-graphics']])
3920 # Check if .magicrc predates the latest and warn if so.
3921 statbuf1 = os.stat(design + '/mag/.magicrc')
3922 statbuf2 = os.stat(pdkdir + '/' + pdkname + '.magicrc')
3923 if statbuf2.st_mtime > statbuf1.st_mtime:
3924 print('NOTE: File .magicrc predates technology startup file. Using default instead.')
3925 magiccommand.extend(['-rcfile', pdkdir + '/' + pdkname + '.magicrc'])
3926 magiccommand.append(designname)
3927
3928 # Run magic and don't wait for it to finish
3929 subprocess.Popen(magiccommand, cwd = design + '/mag')
3930 else:
3931 print("You must first select a project.", file=sys.stderr)
3932
3933 #----------------------------------------------------------------------
3934 # Run the klayout layout editor
3935 #----------------------------------------------------------------------
3936
3937 def edit_layout_with_klayout(self):
3938 value = self.projectselect.selected()
3939 print("Klayout unsupported from project manager (work in progress); run manually", file=sys.stderr)
3940
3941 #----------------------------------------------------------------------
3942 # Run the electric layout editor
3943 #----------------------------------------------------------------------
3944
3945 def edit_layout_with_electric(self):
3946 value = self.projectselect.selected()
3947 print("Electric layout editing unsupported from project manager (work in progress); run manually", file=sys.stderr)
3948
3949 #----------------------------------------------------------------------
3950 # Upload design to the marketplace
3951 # NOTE: This is not being called by anything. Use version in the
3952 # characterization script, which can check for local results before
3953 # approving (or forcing) an upload.
3954 #----------------------------------------------------------------------
3955
3956 def upload(self):
3957 '''
3958 value = self.projectselect.selected()
3959 if value:
3960 design = value['values'][0]
3961 # designname = value['text']
3962 designname = self.project_name
3963 print('Upload design ' + designname + ' (' + design + ' )')
3964 subprocess.run(['/ef/apps/bin/withnet',
3965 og_config.apps_path + '/cace_design_upload.py',
3966 design, '-test'])
3967 '''
3968
3969 #--------------------------------------------------------------------------
3970 # Upload a datasheet to the marketplace (Administrative use only, for now)
3971 #--------------------------------------------------------------------------
3972
3973 # def make_challenge(self):
3974 # importp = self.cur_import
3975 # print("Make a Challenge from import " + importp + "!")
3976 # # subprocess.run([og_config.apps_path + '/cace_import_upload.py', importp, '-test'])
3977
3978 def setcurrent(self, value):
3979 global currdesign
3980 treeview = value.widget
3981 selection = treeview.item(treeview.selection())
3982 pname = selection['text']
3983 #print("setcurrent returned value " + pname)
3984 efmetapath = os.path.expanduser(currdesign)
3985 if not os.path.exists(efmetapath):
3986 os.makedirs(os.path.split(efmetapath)[0], exist_ok=True)
3987 with open(efmetapath, 'w') as f:
3988 f.write(pname + '\n')
3989
3990 # Pick up the PDK from "values", use it to find the PDK folder, determine
3991 # if it has a "magic" subfolder, and enable/disable the "Edit Layout"
3992 # button accordingly
3993
3994 svalues = selection['values']
3995 pdkitems = svalues[1].split()
3996 pdkdir = ''
3997
3998 ef_style=False
3999
4000 if os.path.exists(svalues[0] + '/.config'):
4001 pdkdir = svalues[0] + '/.config/techdir'
4002 elif os.path.exists(svalues[0] + '/.ef-config'):
4003 pdkdir = svalues[0] + '/.ef-config/techdir'
4004 ef_style=True
4005
4006 if pdkdir == '':
4007 print('No pdkname found; layout editing disabled')
4008 self.toppane.appbar.layout_button.config(state='disabled')
4009 else:
4010 try:
4011 if ef_style:
4012 subf = os.listdir(pdkdir + '/libs.tech/magic/current')
4013 else:
4014 subf = os.listdir(pdkdir + '/libs.tech/magic')
4015 except:
4016 print('PDK ' + pdkname + ' has no layout setup; layout editing disabled')
4017 self.toppane.appbar.layout_button.config(state='disabled')
4018 '''
4019 svalues = selection['values'][1]
4020 print('selection: '+str(selection))
4021 pdkitems = svalues.split()
4022 print('Contents of pdkitems: '+str(pdkitems))
4023 pdkname = ''
4024 if ':' in pdkitems:
4025 pdkitems.remove(':')
4026 if len(pdkitems) == 2:
4027 # New behavior Sept. 2017, have to cope with <foundry>.<N> directories, ugh.
4028 pdkdirs = os.listdir('/usr/share/pdk/')
4029 #TODO: pdkdirs = os.listdir('PREFIX/pdk/')
4030
4031 for pdkdir in pdkdirs:
4032 if pdkitems[0] == pdkdir:
4033 pdkname = pdkdir
4034 #TODO: PREFIX
4035 if os.path.exists('/usr/share/pdk/' + pdkname + '/' + pdkitems[1]):
4036 break
4037 else:
4038 pdkpair = pdkdir.split('.')
4039 if pdkpair[0] == pdkitems[0]:
4040 pdkname = pdkdir
4041 #TODO: PREFIX
4042 if os.path.exists('/usr/share/pdk/' + pdkname + '/' + pdkitems[1]):
4043 break
4044 if pdkname == '':
4045 print('No pdkname found; layout editing disabled')
4046 self.toppane.appbar.layout_button.config(state='disabled')
4047 else:
4048 try:
4049 subf = os.listdir('/ef/tech/' + pdkname + '/' + pdkitems[1] + '/libs.tech/magic/current')
4050 except:
4051 print('PDK ' + pdkname + ' has no layout setup; layout editing disabled')
4052 self.toppane.appbar.layout_button.config(state='disabled')
4053 else:
4054 self.toppane.appbar.layout_button.config(state='enabled')
4055 else:
4056 print('No PDK returned in project selection data; layout editing disabled.')
4057 self.toppane.appbar.layout_button.config(state='disabled')
4058 '''
4059 # If the selected project directory has a JSON file and netlists in the "spi"
4060 # and "testbench" folders, then enable the "Characterize" button; else disable
4061 # it.
4062 # NOTE: project.json is the preferred name for the datasheet
4063 # file. However, the .spi file, .delib file, etc., all have the name of the
4064 # project from "ip-name" in the datasheet.
4065 # "<project_folder_name>.json" is the legacy name for the datasheet, deprecated.
4066
4067 found = False
4068 ppath = selection['values'][0]
4069 jsonname = ppath + '/project.json'
4070 legacyname = ppath + '/' + pname + '.json'
4071 if not os.path.isfile(jsonname):
4072 if os.path.isfile(legacyname):
4073 jsonname = legacyname
4074
4075 if os.path.isfile(jsonname):
4076 # Pull the ipname into local store (may want to do this with the
4077 # datasheet as well)
4078 with open(jsonname, 'r') as f:
4079 datatop = json.load(f)
4080 dsheet = datatop['data-sheet']
4081 ipname = dsheet['ip-name']
4082 self.project_name = ipname
4083 found = True
4084
4085 # Do not specifically prohibit opening the characterization app if
4086 # there is no schematic or netlist. Otherwise the user is prevented
4087 # even from seeing the electrical parameters. Let the characterization
4088 # tool allow or prohibit simulation based on this.
4089 # if os.path.exists(ppath + '/spi'):
4090 # if os.path.isfile(ppath + '/spi/' + ipname + '.spi'):
4091 # found = True
4092 #
4093 # if found == False and os.path.exists(ppath + '/elec'):
4094 # if os.path.isdir(ppath + '/elec/' + ipname + '.delib'):
4095 # if os.path.isfile(ppath + '/elec/' + ipname + '.delib/' + ipname + '.sch'):
4096 # found = True
4097 else:
4098 # Use 'pname' as the default project name.
4099 print('No characterization file ' + jsonname)
4100 print('Setting project ip-name from the project folder name.')
4101 self.project_name = pname
4102
4103 # If datasheet has physical parameters but not electrical parameters, then it's okay
4104 # for it not to have a testbench directory; it's still valid. However, having
4105 # neither physical nor electrical parameters means there's nothing to characterize.
4106 if found and 'electrical-params' in dsheet and len(dsheet['electrical-params']) > 0:
4107 if not os.path.isdir(ppath + '/testbench'):
4108 print('No testbench directory for eletrical parameter simulation methods.', file=sys.stderr)
4109 found = False
4110 elif found and not 'physical-params' in dsheet:
4111 print('Characterization file defines no characterization tests.', file=sys.stderr)
4112 found = False
4113 elif found and 'physical-params' in dsheet and len(dsheet['physical-params']) == 0:
4114 print('Characterization file defines no characterization tests.', file=sys.stderr)
4115 found = False
4116
4117 if found == True:
4118 self.toppane.appbar.char_button.config(state='enabled')
4119 else:
4120 self.toppane.appbar.char_button.config(state='disabled')
4121
4122 # Warning: temporary hack (Tim, 1/9/2018)
4123 # Pad frame generator is currently limited to the XH035 cells, so if the
4124 # project PDK is not XH035, disable the pad frame button
4125
4126 if len(pdkitems) > 1 and pdkitems[1] == 'EFXH035B':
4127 self.toppane.appbar.padframeCalc_button.config(state='enabled')
4128 else:
4129 self.toppane.appbar.padframeCalc_button.config(state='disabled')
4130
4131# main app. fyi: there's a 2nd/earlier __main__ section for splashscreen
4132if __name__ == '__main__':
4133 OpenGalaxyManager(root)
4134 if deferLoad:
4135 # Without this, mainloop may find&run very short clock-delayed events BEFORE main form display.
4136 # With it 1st project-load can be scheduled using after-time=0 (needn't tune a delay like 100ms).
4137 root.update_idletasks()
4138 root.mainloop()