blob: c7c3f0fcda67c73e6cec14c1b70bf3484ec2bec3 [file] [log] [blame]
emayecs5656b2b2021-08-04 12:44:13 -04001#!/usr/bin/env python3 -B
emayecs5966a532021-07-29 10:07:02 -04002#
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):
emayecs14748312021-08-05 14:21:26 -040056 """Project Management Splash Screen"""
emayecs5966a532021-07-29 10:07:02 -040057
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
emayecsf31fb7a2021-08-04 16:27:55 -0400103import config
emayecs5966a532021-07-29 10:07:02 -0400104
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):
emayecs55354d82021-08-08 15:57:32 -0400249 def body(self, master, warning, seed='', importnode=None, development=False, parent_pdk=''):
emayecs5966a532021-07-29 10:07:02 -0400250 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/
emayecs582bc382021-08-13 12:32:12 -0400280 (foundry, foundry_name, node, desc, status) = ProjectManager.pdkdir2fnd( pdkdir )
emayecs5966a532021-07-29 10:07:02 -0400281 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
emayecs55354d82021-08-08 15:57:32 -0400297 if parent_pdk != '':
298 pdk_def = parent_pdk
299
emayecs5966a532021-07-29 10:07:02 -0400300 self.pvar.set(pdk_def)
301
302 # Restrict list to single entry if importnode was non-NULL and
303 # is in the PDK list (OptionMenu is replaced by a simple label)
304 # Otherwise, restrict the list to entries having an "status"
305 # entry equal to "active". This allows some legacy PDKs to be
306 # disabled for creating new projects (but available for projects
307 # that already have them).
308
emayecs55354d82021-08-08 15:57:32 -0400309 if importnode or parent_pdk != '':
emayecs5966a532021-07-29 10:07:02 -0400310 self.pdkselect = ttk.Label(master, text = pdk_def, style='blue.TLabel')
311 else:
312 pdkactive = list(item for item in pdklist if self.pdkstat[item] == 'active')
313 if development:
314 pdkactive.extend(list(item for item in pdklist if self.pdkstat[item] == 'development'))
emayecs5966a532021-07-29 10:07:02 -0400315 self.pdkselect = ttk.OptionMenu(master, self.pvar, pdk_def, *pdkactive,
316 style='blue.TMenubutton', command=self.show_info)
317 self.pdkselect.grid(row = 2, column = 1)
318 self.show_info(0)
319
320 return self.nentry # initial focus
321
322 def show_info(self, args):
323 key = str(self.pvar.get())
324 desc = self.pdkdesc[key]
325 if desc == '':
326 self.infolabel.config(text='(no description available)')
327 else:
328 self.infolabel.config(text=desc)
329
330 def apply(self):
331 return self.nentry.get(), self.pdkmap[ str(self.pvar.get()) ] # Note converts StringVar to string
332
333#----------------------------------------------------------------
334# Not-Quite-So-Simple dialog for selecting an existing project.
335# Select a project name from a drop-down list. This could be
336# replaced by simply using the selected (current) project.
337#----------------------------------------------------------------
338
339class ExistingProjectDialog(tksimpledialog.Dialog):
340 def body(self, master, plist, seed, warning='Enter name of existing project to import into:'):
341 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
342
343 # Alphebetize list
344 plist.sort()
345 # Add projects
346 self.pvar = tkinter.StringVar(master)
347 self.pvar.set(plist[0])
348
349 ttk.Label(master, text='Select project:').grid(row = 1, column = 0)
350
351 self.projectselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton')
352 self.projectselect.grid(row = 1, column = 1, sticky = 'ewns')
353 # pack version (below) hangs. Don't know why, changed to grid (like ProjectNameDialog)
354 # self.projectselect.pack(side = 'top', fill = 'both', expand = 'true')
355 return self.projectselect # initial focus
356
357 def apply(self):
358 return self.pvar.get() # Note converts StringVar to string
359
360#----------------------------------------------------------------
361# Not-Quite-So-Simple dialog for selecting an existing ElecLib of existing project.
362# Select an elecLib name from a drop-down list.
363#----------------------------------------------------------------
364
365class ExistingElecLibDialog(tksimpledialog.Dialog):
366 def body(self, master, plist, seed):
367 warning = "Enter name of existing Electric library to import into:"
368 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
369
370 # Alphebetize list
371 plist.sort()
372 # Add electric libraries
373 self.pvar = tkinter.StringVar(master)
374 self.pvar.set(plist[0])
375
376 ttk.Label(master, text="Select library:").grid(row = 1, column = 0)
377
378 self.libselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton')
379 self.libselect.grid(row = 1, column = 1)
380 return self.libselect # initial focus
381
382 def apply(self):
383 return self.pvar.get() # Note converts StringVar to string
384
385#----------------------------------------------------------------
386# Dialog for layout, in case of multiple layout names, none of
387# which matches the project name (ip-name). Method: Select a
388# layout name from a drop-down list. If there is no project.json
389# file, add a checkbox for creating one and seeding the ip-name
390# with the name of the selected layout. Include entry for
391# new layout, and for new layouts add a checkbox to import the
392# layout from schematic or verilog, if a valid candidate exists.
393#----------------------------------------------------------------
394
395class EditLayoutDialog(tksimpledialog.Dialog):
396 def body(self, master, plist, seed='', ppath='', pname='', warning='', hasnet=False):
397 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
398 self.ppath = ppath
399 self.pname = pname
400
401 # Checkbox variable
402 self.confirm = tkinter.IntVar(master)
403 self.confirm.set(0)
404
405 # To-Do: Add checkbox for netlist import
406
407 # Alphebetize list
408 plist.sort()
409 # Add additional item for new layout
410 plist.append('(New layout)')
411
412 # Add layouts to list
413 self.pvar = tkinter.StringVar(master)
414 self.pvar.set(plist[0])
415
416 ttk.Label(master, text='Selected layout to edit:').grid(row = 1, column = 0)
417
418 if pname in plist:
419 pseed = plist.index(pname)
420 else:
421 pseed = 0
422
423 self.layoutselect = ttk.OptionMenu(master, self.pvar, plist[pseed], *plist,
424 style='blue.TMenubutton', command=self.handle_choice)
425 self.layoutselect.grid(row = 1, column = 1, sticky = 'ewns')
426
427 # Create an entry form and checkbox for entering a new layout name, but
428 # keep them unpacked unless the "(New layout)" selection is chosen.
429
430 self.layoutbox = ttk.Frame(master)
431 self.layoutlabel = ttk.Label(self.layoutbox, text='New layout name:')
432 self.layoutlabel.grid(row = 0, column = 0, sticky = 'ewns')
433 self.layoutentry = ttk.Entry(self.layoutbox)
434 self.layoutentry.grid(row = 0, column = 1, sticky = 'ewns')
435 self.layoutentry.insert(0, pname)
436
437 # Only allow 'makeproject' checkbox if there is no project.json file
438 jname = ppath + '/project.json'
439 if not os.path.exists(jname):
emayecsb2487ae2021-08-05 10:30:13 -0400440 self.makeproject = ttk.Checkbutton(self.layoutbox,
emayecs5966a532021-07-29 10:07:02 -0400441 text='Make default project name',
442 variable = self.confirm)
emayecsb2487ae2021-08-05 10:30:13 -0400443 self.makeproject.grid(row = 2, column = 0, columnspan = 2, sticky = 'ewns')
emayecs5966a532021-07-29 10:07:02 -0400444 return self.layoutselect # initial focus
445
446 def handle_choice(self, event):
447 if self.pvar.get() == '(New layout)':
448 # Add entry and checkbox for creating ad-hoc project.json file
449 self.layoutbox.grid(row = 1, column = 0, columnspan = 2, sticky = 'ewns')
450 else:
451 # Remove entry and checkbox
452 self.layoutbox.grid_forget()
453 return
454
455 def apply(self):
456 if self.pvar.get() == '(New layout)':
457 if self.confirm.get() == 1:
458 pname = self.pname
459 master.create_ad_hoc_json(self.layoutentry.get(), pname)
460 return self.layoutentry.get()
461 else:
462 return self.pvar.get() # Note converts StringVar to string
463
464#----------------------------------------------------------------
465# Dialog for padframe: select existing ElecLib of existing project, type in a cellName.
466# Select an elecLib name from a drop-down list.
467# Text field for entry of a cellName.
468#----------------------------------------------------------------
469
470class ExistingElecLibCellDialog(tksimpledialog.Dialog):
471 def body(self, master, descPre, seed='', descPost='', plist=None, seedLibNm=None, seedCellNm=''):
472 warning = 'Pick existing Electric library; enter cell name'
473 warning = (descPre or '') + ((descPre and ': ') or '') + warning + ((descPost and ' ') or '') + (descPost or '')
474 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
475
476 # Alphebetize list
477 plist.sort()
478 # Add electric libraries
479 self.pvar = tkinter.StringVar(master)
480 pNdx = 0
481 if seedLibNm and seedLibNm in plist:
482 pNdx = plist.index(seedLibNm)
483 self.pvar.set(plist[pNdx])
484
485 ttk.Label(master, text='Electric library:').grid(row = 1, column = 0, sticky = 'ens')
486 self.libselect = ttk.OptionMenu(master, self.pvar, plist[pNdx], *plist, style='blue.TMenubutton')
487 self.libselect.grid(row = 1, column = 1, sticky = 'wns')
488
489 ttk.Label(master, text=('cell name:')).grid(row = 2, column = 0, sticky = 'ens')
490 self.nentry = ttk.Entry(master)
491 self.nentry.grid(row = 2, column = 1, sticky = 'ewns')
492 self.nentry.insert(0, seedCellNm)
493
494 return self.libselect # initial focus
495
496 def apply(self):
497 # return list of 2 strings: selected ElecLibName, typed-in cellName.
498 return [self.pvar.get(), self.nentry.get()] # Note converts StringVar to string
499
500#------------------------------------------------------
501# Simple dialog for confirming anything.
502#------------------------------------------------------
503
504class ConfirmDialog(tksimpledialog.Dialog):
505 def body(self, master, warning, seed):
506 if warning:
507 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
508 return self
509
510 def apply(self):
511 return 'okay'
512
513#------------------------------------------------------
514# More proactive dialog for confirming an invasive
515# procedure like "delete project". Requires user to
516# click a checkbox to ensure this is not a mistake.
517# confirmPrompt can be overridden, default='I am sure I want to do this.'
518#------------------------------------------------------
519
520class ProtectedConfirmDialog(tksimpledialog.Dialog):
521 def body(self, master, warning, seed='', confirmPrompt=None):
522 if warning:
523 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
524 self.confirm = tkinter.IntVar(master)
525 self.confirm.set(0)
526 if not confirmPrompt:
527 confirmPrompt='I am sure I want to do this.'
528 ttk.Checkbutton(master, text=confirmPrompt,
529 variable = self.confirm).grid(row = 1, columnspan = 2, sticky = 'enws')
530 return self
531
532 def apply(self):
533 return 'okay' if self.confirm.get() == 1 else ''
534
535#------------------------------------------------------
536# Simple dialog to say "blah is not implemented yet."
537#------------------------------------------------------
538
539class NotImplementedDialog(tksimpledialog.Dialog):
540 def body(self, master, warning, seed):
541 if not warning:
542 warning = "Sorry, that feature is not implemented yet"
543 if warning:
544 warning = "Sorry, " + warning + ", is not implemented yet"
545 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
546 return self
547
548 def apply(self):
549 return 'okay'
550
551#------------------------------------------------------
552# (This is actually a generic confirm dialogue, no install/overwrite intelligence)
553# But so far dedicated to confirming the installation of one or more files,
554# with notification of which (if any) will overwrite existing files.
555#
556# The warning parameter is fully constructed by caller, as multiple lines as either:
557# For the import of module 'blah',
558# CONFIRM installation of (*: OVERWRITE existing):
559# * path1
560# path2
561# ....
562# or:
563# For the import of module 'blah',
564# CONFIRM installation of:
565# path1
566# path2
567# ....
568# TODO: bastardizes warning parameter as multiple lines. Implement some other way?
569#------------------------------------------------------
570
571class ConfirmInstallDialog(tksimpledialog.Dialog):
572 def body(self, master, warning, seed):
573 if warning:
574 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
575 return self
576
577 def apply(self):
578 return 'okay'
579
580#------------------------------------------------------
emayecs12e85282021-08-11 09:37:00 -0400581# Dialog to import a project into the project manager
582#------------------------------------------------------
583
584class ImportDialog(tksimpledialog.Dialog):
emayecs582bc382021-08-13 12:32:12 -0400585 def body(self, master, warning, seed, parent_pdk, parent_path, project_dir):
586 self.badrex1 = re.compile("^\.")
587 self.badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
588
589 self.projectpath = ""
590 self.project_pdkdir = ""
591 self.foundry = ""
592 self.node = ""
593 self.parentpdk = parent_pdk
594 self.parentpath = parent_path
595 self.projectdir = project_dir #folder that contains all projects
596
emayecs12e85282021-08-11 09:37:00 -0400597 if warning:
emayecsa71088b2021-08-14 18:02:58 -0400598 ttk.Label(master, text=warning, wraplength=250).grid(row = 0, columnspan = 2, sticky = 'wns')
emayecs12e85282021-08-11 09:37:00 -0400599 ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0)
emayecs582bc382021-08-13 12:32:12 -0400600
601 self.entry_v = tkinter.StringVar()
602
603 self.nentry = ttk.Entry(master, textvariable = self.entry_v)
emayecs12e85282021-08-11 09:37:00 -0400604 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
emayecs582bc382021-08-13 12:32:12 -0400605
606 self.entry_v.trace('w', self.text_validate)
607
608
emayecs12e85282021-08-11 09:37:00 -0400609 ttk.Button(master,
610 text = "Choose Project...",
611 command = self.browseFiles).grid(row = 3, column = 0)
612
emayecsa71088b2021-08-14 18:02:58 -0400613 self.pathlabel = ttk.Label(master, text = ("No project selected" if self.projectpath =="" else self.projectpath), style = 'red.TLabel', wraplength=300)
emayecs12e85282021-08-11 09:37:00 -0400614
615 self.pathlabel.grid(row = 3, column = 1)
616
emayecs582bc382021-08-13 12:32:12 -0400617 ttk.Label(master, text="Foundry/node:").grid(row = 4, column = 0)
618
619 self.pdklabel = ttk.Label(master, text="N/A", style = 'red.TLabel')
620 self.pdklabel.grid(row = 4, column = 1)
621
622 self.importoption = tkinter.StringVar()
623
624 self.importoption.set(("copy" if parent_pdk!='' else "link"))
625
626 self.linkbutton = ttk.Radiobutton(master, text="Make symbolic link", variable=self.importoption, value="link")
627 self.linkbutton.grid(row = 5, column = 0)
628 ttk.Radiobutton(master, text="Copy project", variable=self.importoption, value="copy").grid(row = 5, column = 1)
629
630 self.error_label = ttk.Label(master, text="", style = 'red.TLabel', wraplength=300)
631 self.error_label.grid(row = 6, column = 0, columnspan = 2)
632
633 self.entry_error = ttk.Label(master, text="", style = 'red.TLabel', wraplength=300)
634 self.entry_error.grid(row = 2, column = 0, columnspan = 2)
635
emayecs12e85282021-08-11 09:37:00 -0400636 return self.nentry
637
emayecs582bc382021-08-13 12:32:12 -0400638 def text_validate(self, *args):
639 newname = self.entry_v.get()
640 projectpath = ''
641 if self.parentpath!='':
642 projectpath = self.parentpath + '/subcells/' + newname
643 else:
644 projectpath = self.projectdir + '/' + newname
645
646 if ProjectManager.blacklisted( newname):
647 self.entry_error.configure(text = newname + ' is not allowed for a project name.')
648 elif newname == "":
649 self.entry_error.configure(text = "")
650 elif self.badrex1.match(newname):
651 self.entry_error.configure(text = 'project name may not start with "."')
652 elif self.badrex2.match(newname):
653 self.entry_error.configure(text = 'project name contains illegal characters or whitespace.')
654 elif os.path.exists(projectpath):
655 self.entry_error.configure(text = newname + ' is already a project name.')
656 else:
657 self.entry_error.configure(text = '')
658 return True
659 return False
660
661 def validate(self, *args):
662 return self.text_validate(self) and self.pdk_validate(self)
663
emayecs12e85282021-08-11 09:37:00 -0400664 def browseFiles(self):
665 initialdir = "~/"
666 if os.path.isdir(self.projectpath):
667 initialdir = os.path.split(self.projectpath)[0]
668
emayecs582bc382021-08-13 12:32:12 -0400669 selected_dir = filedialog.askdirectory(initialdir = initialdir, title = "Select a Project to Import",)
emayecs12e85282021-08-11 09:37:00 -0400670
671 if os.path.isdir(str(selected_dir)):
emayecs582bc382021-08-13 12:32:12 -0400672 self.error_label.configure(text = '')
673 self.linkbutton.configure(state="normal")
674
emayecs12e85282021-08-11 09:37:00 -0400675 self.projectpath = selected_dir
emayecs582bc382021-08-13 12:32:12 -0400676 self.pathlabel.configure(text=self.projectpath, style = 'blue.TLabel')
emayecs12e85282021-08-11 09:37:00 -0400677 # Change label contents
678 if (self.nentry.get() == ''):
679 self.nentry.insert(0, os.path.split(self.projectpath)[1])
680
emayecs582bc382021-08-13 12:32:12 -0400681 self.pdk_validate(self)
682
683 def pdk_validate(self, *args):
684 if not os.path.exists(self.projectpath):
685 self.error_label.configure(text = 'Invalid directory')
686 return False
emayecsa71088b2021-08-14 18:02:58 -0400687
688 if self.parentpath != "" and self.projectpath in self.parentpath:
689 self.error_label.configure(text = 'Cannot import a parent directory into itself.')
690 return False
emayecs582bc382021-08-13 12:32:12 -0400691 #Find project pdk
692 if os.path.exists(self.projectpath + '/.config/techdir') or os.path.exists(self.projectpath + '/.ef-config/techdir'):
693 self.project_pdkdir = os.path.realpath(self.projectpath + ProjectManager.config_path( self.projectpath) + '/techdir')
694 self.foundry, foundry_name, self.node, desc, status = ProjectManager.pdkdir2fnd( self.project_pdkdir )
695 else:
696 if not os.path.exists(self.projectpath + '/info.yaml'):
697 self.error_label.configure(text = self.projectpath + ' does not contain an info.yaml file.')
698 self.project_pdkdir = ""
699 self.foundry = ""
700 self.node = ""
701 else:
702 self.project_pdkdir, self.foundry, self.node = ProjectManager.get_import_pdk( self.projectpath)
703
704 if self.project_pdkdir == "":
705 self.pdklabel.configure(text="Not found", style='red.TLabel')
706 return False
707 else:
708 if (self.parentpdk!="" and self.parentpdk != self.foundry + '/' + self.node):
709 self.importoption.set("copy")
710 self.linkbutton.configure(state="disabled")
711 self.error_label.configure(text = 'Warning: Parent project uses '+self.parentpdk+' instead of '+self.foundry + '/' + self.node+'. The imported project will be copied and cleaned.')
712 self.pdklabel.configure(text=self.foundry + '/' + self.node, style='blue.TLabel')
713 return True
714
715
emayecs12e85282021-08-11 09:37:00 -0400716 def apply(self):
emayecs582bc382021-08-13 12:32:12 -0400717 return self.nentry.get(), self.project_pdkdir, self.projectpath, self.importoption.get()
718
emayecsca79e462021-08-19 16:18:50 -0400719#------------------------------------------------------
emayecs4cd7f232021-08-19 16:22:35 -0400720# Dialog to allow users to select a flow
emayecsca79e462021-08-19 16:18:50 -0400721#------------------------------------------------------
emayecs582bc382021-08-13 12:32:12 -0400722
emayecsca79e462021-08-19 16:18:50 -0400723class SelectFlowDialog(tksimpledialog.Dialog):
724 def body(self, master, warning, seed='', is_subproject = False):
725 self.wait_visibility()
726 if warning:
727 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
728
729 ttk.Label(master, text="Flow:").grid(row = 1, column = 0)
730
731 project_flows = {
732 'Analog':'Schematic, Simulation, Layout, DRC, LVS',
733 'Digital':'Preparation, Synthesis, Placement, Static Timing Analysis, Routing, Post-Route STA, Migration, DRC, LVS, GDS, Cleanup',
734 'Mixed-Signal':'',
735 'Assembly':'',
736 }
737
738 subproject_flows = {
739 'Analog':'Schematic, Simulation, Layout, DRC, LVS',
740 'Digital':'Preparation, Synthesis, Placement, Static Timing Analysis, Routing, Post-Route STA, Migration, DRC, LVS, GDS, Cleanup',
741 'Mixed-Signal': '',
742 }
743 self.flows = subproject_flows if is_subproject else project_flows
744 self.flowvar = tkinter.StringVar(master, value = 'Analog')
745
746 self.infolabel = ttk.Label(master, text=self.flows[self.flowvar.get()], style = 'brown.TLabel', wraplength=250)
747 self.infolabel.grid(row = 2, column = 0, columnspan = 2, sticky = 'news')
748
749 self.option_menu = ttk.OptionMenu(
750 master,
751 self.flowvar,
752 self.flowvar.get(),
753 *self.flows.keys(),
754 command=self.show_info
755 )
756
757 self.option_menu.grid(row = 1, column = 1)
758
759 return self.option_menu# initial focus
760
761 def show_info(self, args):
762 key = self.flowvar.get()
763 print(key)
764 desc = self.flows[key]
765 if desc == '':
766 self.infolabel.config(text='(no description available)')
767 else:
768 self.infolabel.config(text=desc)
769
770
771 def apply(self):
772 return str(self.flowvar.get()) # Note converts StringVar to string
emayecs12e85282021-08-11 09:37:00 -0400773
774#------------------------------------------------------
emayecs14748312021-08-05 14:21:26 -0400775# Project Manager class
emayecs5966a532021-07-29 10:07:02 -0400776#------------------------------------------------------
777
emayecs582bc382021-08-13 12:32:12 -0400778class ProjectManager(ttk.Frame):
emayecs14748312021-08-05 14:21:26 -0400779 """Project Management GUI."""
emayecs5966a532021-07-29 10:07:02 -0400780
781 def __init__(self, parent, *args, **kwargs):
782 super().__init__(parent, *args, **kwargs)
783 self.root = parent
784 parent.withdraw()
785 # self.update()
786 self.update_idletasks() # erase small initial frame asap
787 self.init_gui()
788 parent.protocol("WM_DELETE_WINDOW", self.on_quit)
789 if splash:
790 splash.destroy()
791 parent.deiconify()
792
793 def on_quit(self):
794 """Exits program."""
795 quit()
796
797 def init_gui(self):
798 """Builds GUI."""
799 global designdir
800 global importdir
801 global archiveimportdir
802 global currdesign
803 global theProg
804 global deferLoad
805
806 message = []
807 allPaneOpen = False
808 prjPaneMinh = 10
809 iplPaneMinh = 4
810 impPaneMinh = 4
811
812 # if deferLoad: # temp. for testing... open all panes
813 # allPaneOpen = True
814
815 # Read user preferences
816 self.prefs = {}
817 self.read_prefs()
818
819 # Get default font size from user preferences
820 fontsize = self.prefs['fontsize']
821
822 s = ttk.Style()
823 available_themes = s.theme_names()
824 # print("themes: " + str(available_themes))
825 s.theme_use(available_themes[0])
826
827 s.configure('gray.TFrame', background='gray40')
828 s.configure('blue_white.TFrame', bordercolor = 'blue', borderwidth = 3)
829 s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'))
830 s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
831 foreground = 'brown', anchor = 'center')
832 s.configure('title2.TLabel', font=('Helvetica', fontsize, 'bold italic'),
833 foreground = 'blue')
834 s.configure('normal.TLabel', font=('Helvetica', fontsize))
835 s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
836 s.configure('brown.TLabel', font=('Helvetica', fontsize), foreground = 'brown3', background = 'gray95')
837 s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3')
838 s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
839 s.configure('normal.TButton', font=('Helvetica', fontsize), border = 3, relief = 'raised')
840 s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red', border = 3,
841 relief = 'raised')
842 s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3', border = 3,
843 relief = 'raised')
844 s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue', border = 3,
845 relief = 'raised')
846
847 # Create the help window
848 self.help = HelpWindow(self, fontsize=fontsize)
849
850 with io.StringIO() as buf, contextlib.redirect_stdout(buf):
emayecsf31fb7a2021-08-04 16:27:55 -0400851 self.help.add_pages_from_file(config.apps_path + '/manager_help.txt')
emayecs5966a532021-07-29 10:07:02 -0400852 message = buf.getvalue()
853
854
855 # Set the help display to the first page
856 self.help.page(0)
857
858 # Create the profile settings window
859 self.profile = Profile(self, fontsize=fontsize)
860
861 # Variables used by option menus
862 self.seltype = tkinter.StringVar(self)
863 self.cur_project = tkinter.StringVar(self)
864 self.cur_import = "(nothing selected)"
865 self.project_name = ""
866
867 # Root window title
emayecs14748312021-08-05 14:21:26 -0400868 self.root.title('Project Manager')
emayecs5966a532021-07-29 10:07:02 -0400869 self.root.option_add('*tearOff', 'FALSE')
870 self.pack(side = 'top', fill = 'both', expand = 'true')
871
872 pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6)
873 pane.pack(side = 'top', fill = 'both', expand = 'true')
874 self.toppane = ttk.Frame(pane)
875 self.botpane = ttk.Frame(pane)
876
877 # All interior windows size to toppane
878 self.toppane.columnconfigure(0, weight = 1)
879 # Projects window resizes preferably to others
880 self.toppane.rowconfigure(3, weight = 1)
881
882 # Get username, and from it determine the project directory.
883 # Save this path, because it gets used often.
884 username = self.prefs['username']
885 self.projectdir = os.path.expanduser('~/' + designdir)
886 self.cloudvdir = os.path.expanduser('~/' + cloudvdir)
887
888 # Check that the project directory exists, and create it if not
889 if not os.path.isdir(self.projectdir):
890 os.makedirs(self.projectdir)
891
892 # Label with the user
893 self.toppane.user_frame = ttk.Frame(self.toppane)
894 self.toppane.user_frame.grid(row = 0, sticky = 'news')
895
896 # Put logo image in corner. Ignore if something goes wrong, as this
897 # is only decorative. Note: ef_logo must be kept as a record in self,
898 # or else it gets garbage collected.
899 try:
900 #EFABLESS PLATFORM
901 self.ef_logo = tkinter.PhotoImage(file='/ef/efabless/opengalaxy/efabless_logo_small.gif')
902 self.toppane.user_frame.logo = ttk.Label(self.toppane.user_frame, image=self.ef_logo)
903 self.toppane.user_frame.logo.pack(side = 'left', padx = 5)
904 except:
905 pass
906
907 self.toppane.user_frame.title = ttk.Label(self.toppane.user_frame, text='User:', style='red.TLabel')
908 self.toppane.user_frame.user = ttk.Label(self.toppane.user_frame, text=username, style='blue.TLabel')
909
910 self.toppane.user_frame.title.pack(side = 'left', padx = 5)
911 self.toppane.user_frame.user.pack(side = 'left', padx = 5)
912
913 #---------------------------------------------
914 ttk.Separator(self.toppane, orient='horizontal').grid(row = 1, sticky = 'news')
915 #---------------------------------------------
916
917 # List of projects:
918 self.toppane.design_frame = ttk.Frame(self.toppane)
919 self.toppane.design_frame.grid(row = 2, sticky = 'news')
920
921 self.toppane.design_frame.design_header = ttk.Label(self.toppane.design_frame, text='Projects',
922 style='title.TLabel')
923 self.toppane.design_frame.design_header.pack(side = 'left', padx = 5)
924
925 self.toppane.design_frame.design_header2 = ttk.Label(self.toppane.design_frame,
926 text='(' + self.projectdir + '/)', style='normal.TLabel')
927 self.toppane.design_frame.design_header2.pack(side = 'left', padx = 5)
928
emayecs55354d82021-08-08 15:57:32 -0400929 # Get current project from ~/.open_pdks/currdesign and set the selection.
emayecs5966a532021-07-29 10:07:02 -0400930 try:
931 with open(os.path.expanduser(currdesign), 'r') as f:
emayecs55354d82021-08-08 15:57:32 -0400932 pdirCur = f.read().rstrip()
emayecs5966a532021-07-29 10:07:02 -0400933 except:
emayecs55354d82021-08-08 15:57:32 -0400934 pdirCur = None
emayecs5966a532021-07-29 10:07:02 -0400935
emayecs55354d82021-08-08 15:57:32 -0400936
emayecs5966a532021-07-29 10:07:02 -0400937 # Create listbox of projects
938 projectlist = self.get_project_list() if not deferLoad else []
939 height = min(10, max(prjPaneMinh, 2 + len(projectlist)))
emayecs55354d82021-08-08 15:57:32 -0400940 self.projectselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, selectVal=pdirCur, natSort=True)
emayecs5966a532021-07-29 10:07:02 -0400941 self.projectselect.populate("Available Projects:", projectlist,
emayecsc707a0a2021-08-06 17:13:27 -0400942 [["New", True, self.createproject],
emayecs12e85282021-08-11 09:37:00 -0400943 ["Import", True, self.importproject],
emayecsca79e462021-08-19 16:18:50 -0400944 ["Flow", False, self.startflow],
emayecs5966a532021-07-29 10:07:02 -0400945 ["Copy", False, self.copyproject],
emayecsc707a0a2021-08-06 17:13:27 -0400946 ["Rename", False, self.renameproject],
emayecsa71088b2021-08-14 18:02:58 -0400947 ["Delete", False, self.deleteproject],
948 ],
emayecs5966a532021-07-29 10:07:02 -0400949 height=height, columns=[0, 1])
950 self.projectselect.grid(row = 3, sticky = 'news')
951 self.projectselect.bindselect(self.setcurrent)
952
emayecsc707a0a2021-08-06 17:13:27 -0400953 tooltip.ToolTip(self.projectselect.get_button(0), text="Create a new project/subproject")
emayecs12e85282021-08-11 09:37:00 -0400954 tooltip.ToolTip(self.projectselect.get_button(1), text="Import a project/subproject")
955 tooltip.ToolTip(self.projectselect.get_button(2), text="Start design flow")
956 tooltip.ToolTip(self.projectselect.get_button(3), text="Make a copy of an entire project")
957 tooltip.ToolTip(self.projectselect.get_button(4), text="Rename a project folder")
958 tooltip.ToolTip(self.projectselect.get_button(5), text="Delete an entire project")
emayecs5966a532021-07-29 10:07:02 -0400959
960 pdklist = self.get_pdk_list(projectlist)
961 self.projectselect.populate2("PDK", projectlist, pdklist)
962
emayecs55354d82021-08-08 15:57:32 -0400963 if pdirCur:
emayecs5966a532021-07-29 10:07:02 -0400964 try:
emayecs55354d82021-08-08 15:57:32 -0400965 curitem = next(item for item in projectlist if pdirCur == item)
emayecs5966a532021-07-29 10:07:02 -0400966 except StopIteration:
967 pass
968 else:
969 if curitem:
emayecs55354d82021-08-08 15:57:32 -0400970 self.projectselect.setselect(pdirCur)
emayecs5966a532021-07-29 10:07:02 -0400971
972 # Check that the import directory exists, and create it if not
973 if not os.path.isdir(self.projectdir + '/' + importdir):
974 os.makedirs(self.projectdir + '/' + importdir)
975
976 # Create a watchdog on the project and import directories
977 watchlist = [self.projectdir, self.projectdir + '/' + importdir]
978 if os.path.isdir(self.projectdir + '/upload'):
979 watchlist.append(self.projectdir + '/upload')
980
981 # Check the creation time of the project manager app itself. Because the project
982 # manager tends to be left running indefinitely, it is important to know when it
983 # has been updated. This is checked once every hour since it is really expected
984 # only to happen occasionally.
985
986 thisapp = [theProg]
987 self.watchself = WatchClock(self, thisapp, self.update_alert, 3600000)
988
989 #---------------------------------------------
990
991 # Add second button bar for major project applications
992 self.toppane.apptitle = ttk.Label(self.toppane, text='Tools:', style='title2.TLabel')
993 self.toppane.apptitle.grid(row = 4, sticky = 'news')
994 self.toppane.appbar = ttk.Frame(self.toppane)
995 self.toppane.appbar.grid(row = 5, sticky = 'news')
996
997 # Define the application buttons and actions
998 self.toppane.appbar.schem_button = ttk.Button(self.toppane.appbar, text='Edit Schematic',
999 command=self.edit_schematic, style = 'normal.TButton')
1000 self.toppane.appbar.schem_button.pack(side = 'left', padx = 5)
1001 self.toppane.appbar.layout_button = ttk.Button(self.toppane.appbar, text='Edit Layout',
1002 command=self.edit_layout, style = 'normal.TButton')
1003 self.toppane.appbar.layout_button.pack(side = 'left', padx = 5)
1004 self.toppane.appbar.lvs_button = ttk.Button(self.toppane.appbar, text='Run LVS',
1005 command=self.run_lvs, style = 'normal.TButton')
1006 self.toppane.appbar.lvs_button.pack(side = 'left', padx = 5)
1007 self.toppane.appbar.char_button = ttk.Button(self.toppane.appbar, text='Characterize',
1008 command=self.characterize, style = 'normal.TButton')
1009 self.toppane.appbar.char_button.pack(side = 'left', padx = 5)
1010 self.toppane.appbar.synth_button = ttk.Button(self.toppane.appbar, text='Synthesis Flow',
1011 command=self.synthesize, style = 'normal.TButton')
1012 self.toppane.appbar.synth_button.pack(side = 'left', padx = 5)
1013
1014 self.toppane.appbar.padframeCalc_button = ttk.Button(self.toppane.appbar, text='Pad Frame',
1015 command=self.padframe_calc, style = 'normal.TButton')
1016 self.toppane.appbar.padframeCalc_button.pack(side = 'left', padx = 5)
1017 '''
1018 if self.prefs['schemeditor'] == 'xcircuit':
1019 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor")
1020 elif self.prefs['schemeditor'] == 'xschem':
1021 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor")
1022 else:
1023 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor")
1024
1025 if self.prefs['layouteditor'] == 'klayout':
1026 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor")
1027 else:
1028 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor")
1029 '''
1030 self.refreshToolTips()
1031
1032 tooltip.ToolTip(self.toppane.appbar.lvs_button, text="Start LVS tool")
1033 tooltip.ToolTip(self.toppane.appbar.char_button, text="Start Characterization tool")
1034 tooltip.ToolTip(self.toppane.appbar.synth_button, text="Start Digital Synthesis tool")
1035 tooltip.ToolTip(self.toppane.appbar.padframeCalc_button, text="Start Pad Frame Generator")
1036
1037 #---------------------------------------------
1038 ttk.Separator(self.toppane, orient='horizontal').grid(row = 6, sticky = 'news')
1039 #---------------------------------------------
1040 # List of IP libraries:
emayecs12e85282021-08-11 09:37:00 -04001041 '''
emayecs5966a532021-07-29 10:07:02 -04001042 self.toppane.library_frame = ttk.Frame(self.toppane)
1043 self.toppane.library_frame.grid(row = 7, sticky = 'news')
1044
1045 self.toppane.library_frame.library_header = ttk.Label(self.toppane.library_frame, text='IP Library:',
1046 style='title.TLabel')
1047 self.toppane.library_frame.library_header.pack(side = 'left', padx = 5)
1048
1049 self.toppane.library_frame.library_header2 = ttk.Label(self.toppane.library_frame,
1050 text='(' + self.projectdir + '/ip/)', style='normal.TLabel')
1051 self.toppane.library_frame.library_header2.pack(side = 'left', padx = 5)
1052
1053 self.toppane.library_frame.library_header3 = ttk.Button(self.toppane.library_frame,
1054 text=(allPaneOpen and '-' or '+'), command=self.library_toggle, style = 'normal.TButton', width = 2)
1055 self.toppane.library_frame.library_header3.pack(side = 'right', padx = 5)
1056
1057 # Create listbox of IP libraries
1058 iplist = self.get_library_list() if not deferLoad else []
1059 height = min(8, max(iplPaneMinh, 2 + len(iplist)))
1060 self.ipselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, natSort=True)
1061 self.ipselect.populate("IP Library:", iplist,
1062 [], height=height, columns=[0, 1], versioning=True)
1063 valuelist = self.ipselect.getvaluelist()
1064 datelist = self.get_date_list(valuelist)
1065 itemlist = self.ipselect.getlist()
1066 self.ipselect.populate2("date", itemlist, datelist)
1067 if allPaneOpen:
1068 self.library_open()
emayecs12e85282021-08-11 09:37:00 -04001069
emayecs5966a532021-07-29 10:07:02 -04001070
1071 #---------------------------------------------
1072 ttk.Separator(self.toppane, orient='horizontal').grid(row = 9, sticky = 'news')
emayecs12e85282021-08-11 09:37:00 -04001073
emayecs5966a532021-07-29 10:07:02 -04001074 #---------------------------------------------
1075 # List of imports:
1076 self.toppane.import_frame = ttk.Frame(self.toppane)
1077 self.toppane.import_frame.grid(row = 10, sticky = 'news')
1078
1079 self.toppane.import_frame.import_header = ttk.Label(self.toppane.import_frame, text='Imports:',
1080 style='title.TLabel')
1081 self.toppane.import_frame.import_header.pack(side = 'left', padx = 5)
1082
1083 self.toppane.import_frame.import_header2 = ttk.Label(self.toppane.import_frame,
1084 text='(' + self.projectdir + '/import/)', style='normal.TLabel')
1085 self.toppane.import_frame.import_header2.pack(side = 'left', padx = 5)
1086
1087 self.toppane.import_frame.import_header3 = ttk.Button(self.toppane.import_frame,
1088 text=(allPaneOpen and '-' or '+'), command=self.import_toggle, style = 'normal.TButton', width = 2)
1089 self.toppane.import_frame.import_header3.pack(side = 'right', padx = 5)
1090
1091 # Create listbox of imports
1092 importlist = self.get_import_list() if not deferLoad else []
1093 self.number_of_imports = len(importlist) if not deferLoad else None
1094 height = min(8, max(impPaneMinh, 2 + len(importlist)))
1095 self.importselect = TreeViewChoice(self.toppane, fontsize=fontsize, markDir=True, deferLoad=deferLoad)
1096 self.importselect.populate("Pending Imports:", importlist,
1097 [["Import As", False, self.importdesign],
1098 ["Import Into", False, self.importintodesign],
1099 ["Delete", False, self.deleteimport]], height=height, columns=[0, 1])
1100 valuelist = self.importselect.getvaluelist()
1101 datelist = self.get_date_list(valuelist)
1102 itemlist = self.importselect.getlist()
1103 self.importselect.populate2("date", itemlist, datelist)
1104
1105 tooltip.ToolTip(self.importselect.get_button(0), text="Import as a new project")
1106 tooltip.ToolTip(self.importselect.get_button(1), text="Import into an existing project")
1107 tooltip.ToolTip(self.importselect.get_button(2), text="Remove the import file(s)")
1108 if allPaneOpen:
1109 self.import_open()
emayecs12e85282021-08-11 09:37:00 -04001110 '''
emayecs5966a532021-07-29 10:07:02 -04001111 #---------------------------------------------
1112 # ttk.Separator(self, orient='horizontal').grid(column = 0, row = 8, columnspan=4, sticky='ew')
1113 #---------------------------------------------
1114
1115 # Add a text window below the import to capture output. Redirect
1116 # print statements to it.
1117 self.botpane.console = ttk.Frame(self.botpane)
1118 self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true')
1119
1120 self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4)
1121 self.text_box.pack(side='left', fill='both', expand = 'true')
1122 console_scrollbar = ttk.Scrollbar(self.botpane.console)
1123 console_scrollbar.pack(side='right', fill='y')
1124 # attach console to scrollbar
1125 self.text_box.config(yscrollcommand = console_scrollbar.set)
1126 console_scrollbar.config(command = self.text_box.yview)
1127
1128 # Give all the expansion weight to the message window.
1129 # self.rowconfigure(9, weight = 1)
1130 # self.columnconfigure(0, weight = 1)
1131
1132 # at bottom (legacy mode): window height grows by one row.
1133 # at top the buttons share a row with user name, reduce window height, save screen real estate.
1134 bottomButtons = False
1135
1136 # Add button bar: at the bottom of window (legacy mode), or share top row with user-name
1137 if bottomButtons:
1138 bbar = ttk.Frame(self.botpane)
1139 bbar.pack(side='top', fill = 'x')
1140 else:
1141 bbar = self.toppane.user_frame
1142
1143 # Define help button
1144 bbar.help_button = ttk.Button(bbar, text='Help',
1145 command=self.help.open, style = 'normal.TButton')
1146
1147 # Define profile settings button
1148 bbar.profile_button = ttk.Button(bbar, text='Settings',
1149 command=self.profile.open, style = 'normal.TButton')
1150
1151 # Define the "quit" button and action
1152 bbar.quit_button = ttk.Button(bbar, text='Quit', command=self.on_quit,
1153 style = 'normal.TButton')
1154 # Tool tips for button bar
1155 tooltip.ToolTip(bbar.quit_button, text="Exit the project manager")
1156 tooltip.ToolTip(bbar.help_button, text="Show help window")
1157
1158 if bottomButtons:
1159 bbar.help_button.pack(side = 'left', padx = 5)
1160 bbar.profile_button.pack(side = 'left', padx = 5)
1161 bbar.quit_button.pack(side = 'right', padx = 5)
1162 else:
1163 # quit at TR like window-title's close; help towards the outside, settings towards inside
1164 bbar.quit_button.pack(side = 'right', padx = 5)
1165 bbar.help_button.pack(side = 'right', padx = 5)
1166 bbar.profile_button.pack(side = 'right', padx = 5)
1167
1168 # Add the panes once the internal geometry is known
1169 pane.add(self.toppane)
1170 pane.add(self.botpane)
1171 pane.paneconfig(self.toppane, stretch='first')
1172 # self.update_idletasks()
1173
1174 #---------------------------------------------------------------
1175 # Project list
1176 # projects = os.listdir(os.path.expanduser('~/' + designdir))
1177 # self.cur_project.set(projects[0])
1178 # self.design_select = ttk.OptionMenu(self, self.cur_project, projects[0], *projects,
1179 # style='blue.TMenubutton')
1180
1181 # New import list
1182 # self.import_select = ttk.Button(self, text=self.cur_import, command=self.choose_import)
1183
1184 #---------------------------------------------------------
1185 # Define project design actions
1186 # self.design_actions = ttk.Frame(self)
1187 # self.design_actions.characterize = ttk.Button(self.design_actions,
1188 # text='Upload and Characterize', command=self.characterize)
1189 # self.design_actions.characterize.grid(column = 0, row = 0)
1190
1191 # Define import actions
1192 # self.import_actions = ttk.Frame(self)
1193 # self.import_actions.upload = ttk.Button(self.import_actions,
1194 # text='Upload Challenge', command=self.make_challenge)
1195 # self.import_actions.upload.grid(column = 0, row = 0)
1196
1197 self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000,
1198 0 if deferLoad else None) # do immediate forced refresh (1st in mainloop)
1199 # self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000)
1200
1201 # Redirect stdout and stderr to the console as the last thing to do. . .
1202 # Otherwise errors in the GUI get sucked into the void.
1203 self.stdout = sys.stdout
1204 self.stderr = sys.stderr
1205 sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
1206 sys.stderr = ConsoleText.StderrRedirector(self.text_box)
1207
1208 if message:
1209 print(message)
1210
1211 if self.prefs == {}:
1212 print("No user preferences file, using default settings.")
1213
1214 # helper for Profile to do live mods of some of the user-prefs (without restart projectManager):
1215 def setUsername(self, newname):
1216 self.toppane.user_frame.user.config(text=newname)
1217
1218 def refreshToolTips(self):
1219 if self.prefs['schemeditor'] == 'xcircuit':
1220 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor")
1221 elif self.prefs['schemeditor'] == 'xschem':
1222 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor")
1223 else:
1224 tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor")
1225
1226 if self.prefs['layouteditor'] == 'klayout':
1227 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor")
1228 else:
1229 tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor")
emayecs582bc382021-08-13 12:32:12 -04001230
1231 @classmethod
1232 def config_path(cls, path):
emayecsc707a0a2021-08-06 17:13:27 -04001233 #returns the config directory that 'path' contains between .config and .ef-config
emayecs5966a532021-07-29 10:07:02 -04001234 if (os.path.exists(path + '/.config')):
1235 return '/.config'
1236 elif (os.path.exists(path + '/.ef-config')):
1237 return '/.ef-config'
emayecs12e85282021-08-11 09:37:00 -04001238 raise Exception('Neither '+path+'/.config nor '+path+'/.ef-config exists.')
emayecs5966a532021-07-29 10:07:02 -04001239
1240 #------------------------------------------------------------------------
1241 # Check if a name is blacklisted for being a project folder
1242 #------------------------------------------------------------------------
emayecs582bc382021-08-13 12:32:12 -04001243
1244 @classmethod
1245 def blacklisted(cls, dirname):
emayecs5966a532021-07-29 10:07:02 -04001246 # Blacklist: Do not show files of these names:
emayecsc707a0a2021-08-06 17:13:27 -04001247 blacklist = [importdir, 'ip', 'upload', 'export', 'lost+found', 'subcells']
emayecs5966a532021-07-29 10:07:02 -04001248 if dirname in blacklist:
1249 return True
1250 else:
1251 return False
1252
1253 def write_prefs(self):
1254 global prefsfile
1255
1256 if self.prefs:
1257 expprefsfile = os.path.expanduser(prefsfile)
1258 prefspath = os.path.split(expprefsfile)[0]
1259 if not os.path.exists(prefspath):
1260 os.makedirs(prefspath)
1261 with open(os.path.expanduser(prefsfile), 'w') as f:
1262 json.dump(self.prefs, f, indent = 4)
1263
1264 def read_prefs(self):
1265 global prefsfile
1266
1267 # Set all known defaults even if they are not in the JSON file so
1268 # that it is not necessary to check for the existence of the keyword
1269 # in the dictionary every time it is accessed.
1270 if 'fontsize' not in self.prefs:
1271 self.prefs['fontsize'] = 11
1272 userid = os.environ['USER']
1273 uid = ''
1274 username = userid
1275 self.prefs['username'] = username
1276
1277 '''
1278 if 'username' not in self.prefs:
1279
1280 #
1281 #EFABLESS PLATFORM
1282 p = subprocess.run(['/ef/apps/bin/withnet' ,
emayecsb2487ae2021-08-05 10:30:13 -04001283 config.apps_path + '/og_uid_service.py', userid],
emayecs5966a532021-07-29 10:07:02 -04001284 stdout = subprocess.PIPE)
1285 if p.stdout:
1286 uid_string = p.stdout.splitlines()[0].decode('utf-8')
1287 userspec = re.findall(r'[^"\s]\S*|".+?"', uid_string)
1288 if len(userspec) > 0:
1289 username = userspec[0].strip('"')
1290 # uid = userspec[1]
1291 # Note userspec[1] = UID and userspec[2] = role, useful
1292 # for future applications.
1293 else:
1294 username = userid
1295 else:
1296 username = userid
1297 self.prefs['username'] = username
1298 # self.prefs['uid'] = uid
1299 '''
1300 if 'schemeditor' not in self.prefs:
1301 self.prefs['schemeditor'] = 'electric'
1302
1303 if 'layouteditor' not in self.prefs:
1304 self.prefs['layouteditor'] = 'magic'
1305
1306 if 'magic-graphics' not in self.prefs:
1307 self.prefs['magic-graphics'] = 'X11'
1308
1309 if 'development' not in self.prefs:
1310 self.prefs['development'] = False
1311
1312 if 'devstdcells' not in self.prefs:
1313 self.prefs['devstdcells'] = False
1314
1315 # Any additional user preferences go above this line.
1316
1317 # Get user preferences from ~/design/.profile/prefs.json and use it to
1318 # overwrite default entries in self.prefs
1319 try:
1320 with open(os.path.expanduser(prefsfile), 'r') as f:
1321 prefsdict = json.load(f)
1322 for key in prefsdict:
1323 self.prefs[key] = prefsdict[key]
1324 except:
1325 # No preferences file, so create an initial one.
1326 if not os.path.exists(prefsfile):
1327 self.write_prefs()
1328
1329 # if 'User:' Label exists, this updates it live (Profile calls read_prefs after write)
1330 try:
1331 self.setUsername(self.prefs['username'])
1332 except:
1333 pass
1334
1335 #------------------------------------------------------------------------
1336 # Get a list of the projects in the user's design directory. Exclude
1337 # items that are not directories, or which are blacklisted.
1338 #------------------------------------------------------------------------
1339
1340 def get_project_list(self):
1341 global importdir
1342
1343 badrex1 = re.compile("^\.")
1344 badrex2 = re.compile(".*[ \t\n].*")
1345
1346 # Get contents of directory. Look only at directories
emayecsc707a0a2021-08-06 17:13:27 -04001347
1348 projectlist = []
1349
emayecs582bc382021-08-13 12:32:12 -04001350 def add_projects(projectpath):
1351 # Recursively add subprojects to projectlist
1352 projectlist.append(projectpath)
1353 #check for subprojects and add them
1354 if os.path.isdir(projectpath + '/subcells'):
1355 for subproj in os.listdir(projectpath + '/subcells'):
1356 if os.path.isdir(projectpath + '/subcells/' + subproj):
1357 add_projects(projectpath + '/subcells/' + subproj)
1358
emayecsc707a0a2021-08-06 17:13:27 -04001359 for item in os.listdir(self.projectdir):
1360 if os.path.isdir(self.projectdir + '/' + item):
1361 projectpath = self.projectdir + '/' + item
emayecs582bc382021-08-13 12:32:12 -04001362 add_projects(projectpath)
emayecsc707a0a2021-08-06 17:13:27 -04001363
1364
emayecs5966a532021-07-29 10:07:02 -04001365 # 'import' and others in the blacklist are not projects!
1366 # Files beginning with '.' and files with whitespace are
1367 # also not listed.
1368 for item in projectlist[:]:
emayecsc707a0a2021-08-06 17:13:27 -04001369 name = os.path.split(item)[1]
1370 if self.blacklisted(name):
emayecs5966a532021-07-29 10:07:02 -04001371 projectlist.remove(item)
emayecsc707a0a2021-08-06 17:13:27 -04001372 elif badrex1.match(name):
emayecs5966a532021-07-29 10:07:02 -04001373 projectlist.remove(item)
emayecsc707a0a2021-08-06 17:13:27 -04001374 elif badrex2.match(name):
emayecs5966a532021-07-29 10:07:02 -04001375 projectlist.remove(item)
emayecs5966a532021-07-29 10:07:02 -04001376 return projectlist
1377
1378 #------------------------------------------------------------------------
1379 # Get a list of the projects in the user's cloudv directory. Exclude
1380 # items that are not directories, or which are blacklisted.
1381 #------------------------------------------------------------------------
1382
1383 def get_cloudv_project_list(self):
1384 global importdir
1385
1386 badrex1 = re.compile("^\.")
1387 badrex2 = re.compile(".*[ \t\n].*")
1388
1389 if not os.path.exists(self.cloudvdir):
1390 print('No user cloudv dir exists; no projects to import.')
1391 return None
1392
1393 # Get contents of cloudv directory. Look only at directories
1394 projectlist = list(item for item in os.listdir(self.cloudvdir) if
1395 os.path.isdir(self.cloudvdir + '/' + item))
1396
1397 # 'import' and others in the blacklist are not projects!
1398 # Files beginning with '.' and files with whitespace are
1399 # also not listed.
1400 for item in projectlist[:]:
1401 if self.blacklisted(item):
1402 projectlist.remove(item)
1403 elif badrex1.match(item):
1404 projectlist.remove(item)
1405 elif badrex2.match(item):
1406 projectlist.remove(item)
1407
1408 # Add pathname to all items in projectlist
1409 projectlist = [self.cloudvdir + '/' + item for item in projectlist]
1410 return projectlist
1411
1412 #------------------------------------------------------------------------
1413 # utility: [re]intialize a project's elec/ dir: the .java preferences and LIBDIRS.
1414 # So user can just delete .java, and restart electric (from projectManager), to reinit preferences.
1415 # So user can just delete LIBDIRS, and restart electric (from projectManager), to reinit LIBDIRS.
1416 # So project copies/imports can filter ngspice/run (and ../.allwaves), we'll recreate it here.
1417 #
1418 # The global /ef/efabless/deskel/* is used and the PDK name substituted.
1419 #
1420 # This SINGLE function is used to setup elec/ contents for new projects, in addition to being
1421 # called in-line prior to "Edit Schematics" (on-the-fly).
1422 #------------------------------------------------------------------------
1423 @classmethod
1424 def reinitElec(cls, design):
1425 pdkdir = os.path.join( design, ".ef-config/techdir")
1426 elec = os.path.join( design, "elec")
1427
1428 # on the fly, ensure has elec/ dir, ensure has ngspice/run/allwaves dir
1429 try:
1430 os.makedirs(design + '/elec', exist_ok=True)
1431 except IOError as e:
1432 print('Error in os.makedirs(elec): ' + str(e))
1433 try:
1434 os.makedirs(design + '/ngspice/run/.allwaves', exist_ok=True)
1435 except IOError as e:
1436 print('Error in os.makedirs(.../.allwaves): ' + str(e))
1437 #EFABLESS PLATFORM
1438 deskel = '/ef/efabless/deskel'
1439
1440 # on the fly:
1441 # .../elec/.java : reinstall if missing. From PDK-specific if any.
1442 if not os.path.exists( os.path.join( elec, '.java')):
1443 # Copy Electric preferences
1444 try:
1445 shutil.copytree(deskel + '/dotjava', design + '/elec/.java', symlinks = True)
1446 except IOError as e:
1447 print('Error copying files: ' + str(e))
1448
1449 # .../elec/LIBDIRS : reinstall if missing, from PDK-specific LIBDIRS
1450 # in libs.tech/elec/LIBDIRS
1451
1452 libdirsloc = pdkdir + '/libs.tech/elec/LIBDIRS'
1453
1454 if not os.path.exists( os.path.join( elec, 'LIBDIRS')):
1455 if os.path.exists( libdirsloc ):
1456 # Copy Electric LIBDIRS
1457 try:
1458 shutil.copy(libdirsloc, design + '/elec/LIBDIRS')
1459 except IOError as e:
1460 print('Error copying files: ' + str(e))
1461 else:
1462 print('Info: PDK not configured for Electric: no libs.tech/elec/LIBDIRS')
1463
1464 return None
1465
1466 #------------------------------------------------------------------------
1467 # utility: filter a list removing: empty strings, strings with any whitespace
1468 #------------------------------------------------------------------------
1469 whitespaceREX = re.compile('\s')
1470 @classmethod
1471 def filterNullOrWS(cls, inlist):
1472 return [ i for i in inlist if i and not cls.whitespaceREX.search(i) ]
1473
1474 #------------------------------------------------------------------------
1475 # utility: do a glob.glob of relative pattern, but specify the rootDir,
1476 # so returns the matching paths found below that rootDir.
1477 #------------------------------------------------------------------------
1478 @classmethod
1479 def globFromDir(cls, pattern, dir=None):
1480 if dir:
1481 dir = dir.rstrip('/') + '/'
1482 pattern = dir + pattern
1483 result = glob.glob(pattern)
1484 if dir and result:
1485 nbr = len(dir)
1486 result = [ i[nbr:] for i in result ]
1487 return result
1488
1489 #------------------------------------------------------------------------
1490 # utility: from a pdkPath, return list of 3 strings: <foundry>, <node>, <description>.
1491 # i.e. pdkPath has form '[.../]<foundry>[.<ext>]/<node>'. For now the description
1492 # is always ''. And an optional foundry extension is pruned/dropped.
1493 # thus '.../XFAB.2/EFXP018A4' -> 'XFAB', 'EFXP018A4', ''
1494 #
1495 # optionally store in each PDK: .ef-config/nodeinfo.json which can define keys:
1496 # 'foundry', 'node', 'description' to override the foundry (computed from the path)
1497 # and (fixed, empty) description currently returned by this.
1498 #
1499 # Intent: keep a short-description field at least, intended to be one-line max 40 chars,
1500 # suitable for a on-hover-tooltip display. (Distinct from a big multiline description).
1501 #
1502 # On error (malformed pdkPath: can't determine foundry or node), the foundry or node
1503 # or both may be '' or as specified in the optional default values (if you're
1504 # generating something for display and want an unknown to appear as 'none' etc.).
1505 #------------------------------------------------------------------------
1506 @classmethod
1507 def pdkdir2fnd(cls, pdkdir, def_foundry='', def_node='', def_description=''):
1508 foundry = ''
emayecs12e85282021-08-11 09:37:00 -04001509 foundry_name = ''
emayecs5966a532021-07-29 10:07:02 -04001510 node = ''
1511 description = ''
1512 status = 'active'
1513 if pdkdir:
emayecsb2487ae2021-08-05 10:30:13 -04001514 # Code should only be for efabless platform
1515 '''
emayecs5966a532021-07-29 10:07:02 -04001516 split = os.path.split(os.path.realpath(pdkdir))
1517 # Full path should be [<something>/]<foundry>[.ext]/<node>
1518 node = split[1]
1519 foundry = os.path.split(split[0])[1]
1520 foundry = os.path.splitext(foundry)[0]
emayecsb2487ae2021-08-05 10:30:13 -04001521 '''
emayecs5966a532021-07-29 10:07:02 -04001522 # Check for nodeinfo.json
1523 infofile = pdkdir + '/.config/nodeinfo.json'
1524 if os.path.exists(infofile):
1525 with open(infofile, 'r') as ifile:
1526 nodeinfo = json.load(ifile)
1527 if 'foundry' in nodeinfo:
1528 foundry = nodeinfo['foundry']
emayecs12e85282021-08-11 09:37:00 -04001529 if 'foundry-name' in nodeinfo:
1530 foundry_name = nodeinfo['foundry-name']
emayecs5966a532021-07-29 10:07:02 -04001531 if 'node' in nodeinfo:
1532 node = nodeinfo['node']
1533 if 'description' in nodeinfo:
1534 description = nodeinfo['description']
1535 if 'status' in nodeinfo:
1536 status = nodeinfo['status']
emayecs12e85282021-08-11 09:37:00 -04001537 return foundry, foundry_name, node, description, status
emayecs5966a532021-07-29 10:07:02 -04001538
1539 infofile = pdkdir + '/.ef-config/nodeinfo.json'
1540 if os.path.exists(infofile):
1541 with open(infofile, 'r') as ifile:
1542 nodeinfo = json.load(ifile)
1543 if 'foundry' in nodeinfo:
1544 foundry = nodeinfo['foundry']
emayecs12e85282021-08-11 09:37:00 -04001545 if 'foundry-name' in nodeinfo:
1546 foundry_name = nodeinfo['foundry-name']
emayecs5966a532021-07-29 10:07:02 -04001547 if 'node' in nodeinfo:
1548 node = nodeinfo['node']
1549 if 'description' in nodeinfo:
1550 description = nodeinfo['description']
1551 if 'status' in nodeinfo:
1552 status = nodeinfo['status']
emayecs12e85282021-08-11 09:37:00 -04001553 return foundry, foundry_name, node, description, status
1554 raise Exception('malformed pdkPath: can\'t determine foundry or node')
emayecs5966a532021-07-29 10:07:02 -04001555
1556 #------------------------------------------------------------------------
1557 # Get a list of the electric-libraries (DELIB only) in a given project.
1558 # List of full-paths each ending in '.delib'
1559 #------------------------------------------------------------------------
1560
1561 def get_elecLib_list(self, pname):
1562 elibs = self.globFromDir(pname + '/elec/*.delib/', self.projectdir)
1563 elibs = [ re.sub("/$", "", i) for i in elibs ]
1564 return self.filterNullOrWS(elibs)
1565
1566 #------------------------------------------------------------------------
1567 # Create a list of datestamps for each import file
1568 #------------------------------------------------------------------------
1569 def get_date_list(self, valuelist):
1570 datelist = []
1571 for value in valuelist:
1572 try:
1573 importfile = value[0]
1574 try:
1575 statbuf = os.stat(importfile)
1576 except:
1577 # Note entries that can't be accessed.
1578 datelist.append("(unknown)")
1579 else:
1580 datestamp = datetime.datetime.fromtimestamp(statbuf.st_mtime)
1581 datestr = datestamp.strftime("%c")
1582 datelist.append(datestr)
1583 except:
1584 datelist.append("(N/A)")
1585
1586 return datelist
1587
1588 #------------------------------------------------------------------------
1589 # Get the PDK attached to a project for display as: '<foundry> : <node>'
1590 # unless path=True: then return true PDK dir-path.
1591 #
1592 # TODO: the ef-config prog output is not used below. Intent was use
1593 # ef-config to be the one official query for *any* project's PDK value, and
1594 # therein-only hide a built-in default for legacy projects without techdir symlink.
1595 # In below ef-config will always give an EF_TECHDIR, so that code-branch always
1596 # says '(default)', the ef-config subproc is wasted, and '(no PDK)' is never
1597 # reached.
1598 #------------------------------------------------------------------------
1599 def get_pdk_dir(self, project, path=False):
1600 pdkdir = os.path.realpath(project + self.config_path(project)+'/techdir')
1601 if path:
1602 return pdkdir
emayecs12e85282021-08-11 09:37:00 -04001603 foundry, foundry_name, node, desc, status = self.pdkdir2fnd( pdkdir )
emayecs5966a532021-07-29 10:07:02 -04001604 return foundry + ' : ' + node
1605 '''
1606 if os.path.isdir(project + '/.ef-config'):
1607 if os.path.exists(project + '/.ef-config/techdir'):
1608 pdkdir = os.path.realpath(project + '/.ef-config/techdir')
1609
1610 elif os.path.isdir(project + '/.config'):
1611 if os.path.exists(project + '/.config/techdir'):
1612 pdkdir = os.path.realpath(project + '/.config/techdir')
1613 if path:
1614 return pdkdir
1615 foundry, node, desc, status = self.pdkdir2fnd( pdkdir )
1616 return foundry + ' : ' + node
1617 '''
1618 '''
1619 if not pdkdir:
1620 # Run "ef-config" script for backward compatibility
1621 export = {'EF_DESIGNDIR': project}
1622 #EFABLESS PLATFORM
1623 p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
1624 stdout = subprocess.PIPE, env = export)
1625 config_out = p.stdout.splitlines()
1626 for line in config_out:
1627 setline = line.decode('utf-8').split('=')
1628 if setline[0] == 'EF_TECHDIR':
1629 pdkdir = ( setline[1] if path else '(default)' )
1630 if not pdkdir:
1631 pdkdir = ( None if path else '(no PDK)' ) # shouldn't get here
1632 '''
1633
1634
1635
1636 return pdkdir
emayecs12e85282021-08-11 09:37:00 -04001637#------------------------------------------------------------------------
emayecs582bc382021-08-13 12:32:12 -04001638 # Get PDK directory for projects without a techdir (most likely the project is being imported)
emayecs5966a532021-07-29 10:07:02 -04001639 #------------------------------------------------------------------------
emayecs582bc382021-08-13 12:32:12 -04001640 @classmethod
1641 def get_import_pdk(cls, projectpath):
1642 print(projectpath)
emayecs12e85282021-08-11 09:37:00 -04001643 yamlname = projectpath + '/info.yaml'
1644
1645 with open(yamlname, 'r') as f:
1646 datatop = yaml.safe_load(f)
1647 project_data = datatop['project']
1648 project_foundry = project_data['foundry']
1649 project_process = project_data['process']
1650
1651 project_pdkdir = ''
1652
1653 for pdkdir_lr in glob.glob('/usr/share/pdk/*/libs.tech/'):
1654 pdkdir = os.path.split( os.path.split( pdkdir_lr )[0])[0]
emayecs582bc382021-08-13 12:32:12 -04001655 foundry, foundry_name, node, desc, status = ProjectManager.pdkdir2fnd( pdkdir )
emayecs12e85282021-08-11 09:37:00 -04001656 if not foundry or not node:
1657 continue
1658 if (foundry == project_foundry or foundry_name == project_foundry) and node == project_process:
1659 project_pdkdir = pdkdir
1660 break
1661
1662 return project_pdkdir, foundry, node #------------------------------------------------------------------------
emayecs5966a532021-07-29 10:07:02 -04001663 # Get the list of PDKs that are attached to each project
1664 #------------------------------------------------------------------------
1665 def get_pdk_list(self, projectlist):
1666 pdklist = []
1667 for project in projectlist:
1668 pdkdir = self.get_pdk_dir(project)
1669 pdklist.append(pdkdir)
emayecsc707a0a2021-08-06 17:13:27 -04001670
emayecs5966a532021-07-29 10:07:02 -04001671 return pdklist
1672
1673 #------------------------------------------------------------------------
1674 # Find a .json's associated tar.gz (or .tgz) if any.
1675 # Return path to the tar.gz if any, else None.
1676 #------------------------------------------------------------------------
1677
1678 def json2targz(self, jsonPath):
1679 root = os.path.splitext(jsonPath)[0]
1680 for ext in ('.tgz', '.tar.gz'):
1681 if os.path.isfile(root + ext):
1682 return root + ext
1683 return None
emayecsb2487ae2021-08-05 10:30:13 -04001684
1685 def yaml2targz(self, yamlPath):
1686 root = os.path.splitext(yamlPath)[0]
1687 for ext in ('.tgz', '.tar.gz'):
1688 if os.path.isfile(root + ext):
1689 return root + ext
1690 return None
emayecs5966a532021-07-29 10:07:02 -04001691
1692 #------------------------------------------------------------------------
1693 # Remove a .json and associated tar.gz (or .tgz) if any.
1694 # If not a .json, remove just that file (no test for a tar).
1695 #------------------------------------------------------------------------
1696
1697 def removeJsonPlus(self, jsonPath):
1698 ext = os.path.splitext(jsonPath)[1]
1699 if ext == ".json":
1700 tar = self.json2targz(jsonPath)
1701 if tar: os.remove(tar)
1702 return os.remove(jsonPath)
1703
1704 #------------------------------------------------------------------------
1705 # MOVE a .json and associated tar.gz (or .tgz) if any, to targetDir.
1706 # If not a .json, move just that file (no test for a tar).
1707 #------------------------------------------------------------------------
1708
1709 def moveJsonPlus(self, jsonPath, targetDir):
1710 ext = os.path.splitext(jsonPath)[1]
1711 if ext == ".json":
1712 tar = self.json2targz(jsonPath)
1713 if tar:
1714 shutil.move(tar, targetDir)
1715 # believe the move throws an error. So return value (the targetDir name) isn't really useful.
1716 return shutil.move(jsonPath, targetDir)
1717
1718 #------------------------------------------------------------------------
1719 # Get a list of the libraries in the user's ip folder
1720 #------------------------------------------------------------------------
1721
1722 def get_library_list(self):
1723 # Get contents of directory
1724 try:
1725 iplist = glob.glob(self.projectdir + '/ip/*/*')
1726 except:
1727 iplist = []
1728 else:
1729 pass
1730
1731 return iplist
1732
1733 #------------------------------------------------------------------------
1734 # Get a list of the files in the user's design import folder
1735 # (use current 'import' but also original 'upload')
1736 #------------------------------------------------------------------------
1737
1738 def get_import_list(self):
1739 # Get contents of directory
1740 importlist = os.listdir(self.projectdir + '/' + importdir)
1741
1742 # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz).
1743 # Also ignore any .swp files dropped by the vim editor.
1744 # Also ignore any subdirectories of import
1745 for item in importlist[:]:
1746 if item[-1] in '#~':
1747 importlist.remove(item)
1748 continue
1749 ipath = self.projectdir + '/' + importdir + '/' + item
1750
1751 # recognize dirs (as u2u projects) if not symlink and has a 'project.json',
1752 # hide dirs named *.bak. If originating user does u2u twice before target user
1753 # can consume/import it, the previous one (only) is retained as *.bak.
1754 if os.path.isdir(ipath):
1755 if os.path.islink(ipath) or not self.validProjectName(item) \
1756 or self.importProjNameBadrex1.match(item) \
emayecsb2487ae2021-08-05 10:30:13 -04001757 or not os.path.isfile(ipath + '/info.yaml'):
emayecs5966a532021-07-29 10:07:02 -04001758 importlist.remove(item)
1759 continue
1760 else:
1761 ext = os.path.splitext(item)
1762 if ext[1] == '.json':
1763 if ext[0] + '.tar.gz' in importlist:
1764 importlist.remove(ext[0] + '.tar.gz')
1765 elif ext[0] + '.tgz' in importlist:
1766 importlist.remove(ext[0] + '.tgz')
1767 elif ext[1] == '.swp':
1768 importlist.remove(item)
1769 elif os.path.isdir(self.projectdir + '/' + importdir + '/' + item):
1770 importlist.remove(item)
1771
1772 # Add pathname to all items in projectlist
1773 importlist = [self.projectdir + '/' + importdir + '/' + item for item in importlist]
1774
1775 # Add support for original "upload" directory (backward compatibility)
1776 if os.path.exists(self.projectdir + '/upload'):
1777 uploadlist = os.listdir(self.projectdir + '/upload')
1778
1779 # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz).
1780 # Also ignore any .swp files dropped by the vim editor.
1781 for item in uploadlist[:]:
1782 ext = os.path.splitext(item)
1783 if ext[1] == '.json':
1784 if ext[0] + '.tar.gz' in uploadlist:
1785 uploadlist.remove(ext[0] + '.tar.gz')
1786 elif ext[0] + '.tgz' in uploadlist:
1787 uploadlist.remove(ext[0] + '.tgz')
1788 elif ext[1] == '.swp':
1789 uploadlist.remove(item)
1790
1791 # Add pathname to all items in projectlist
1792 uploadlist = [self.projectdir + '/upload/' + item for item in uploadlist]
1793 importlist.extend(uploadlist)
1794
1795 # Remember the size of the list so we know when it changed
1796 self.number_of_imports = len(importlist)
1797 return importlist
1798
1799 #------------------------------------------------------------------------
1800 # Import for json documents and related tarballs (.gz or .tgz):
1801 #------------------------------------------------------------------------
1802
emayecsb2487ae2021-08-05 10:30:13 -04001803 def importyaml(self, projname, importfile):
emayecs5966a532021-07-29 10:07:02 -04001804 # (1) Check if there is a tarball with the same root name as the JSON
1805 importroot = os.path.splitext(importfile)[0]
1806 badrex1 = re.compile("^\.")
1807 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
1808 if os.path.isfile(importroot + '.tgz'):
1809 tarname = importroot + '.tgz'
1810 elif os.path.isfile(importroot + '.tar.gz'):
1811 tarname = importroot + '.tar.gz'
1812 else:
1813 tarname = []
1814 # (2) Check for name conflict
1815 origname = projname
1816 newproject = self.projectdir + '/' + projname
1817 newname = projname
1818 while os.path.isdir(newproject) or self.blacklisted(newname):
1819 if self.blacklisted(newname):
1820 warning = "Name " + newname + " is not allowed for a project name."
1821 elif badrex1.match(newname):
1822 warning = 'project name may not start with "."'
1823 elif badrex2.match(newname):
1824 warning = 'project name contains illegal characters or whitespace.'
1825 else:
1826 warning = "Project " + newname + " already exists!"
1827 newname = ProjectNameDialog(self, warning, seed=newname).result
1828 if not newname:
1829 return 0 # Canceled, no action.
1830 newproject = self.projectdir + '/' + newname
1831 print("New project name is " + newname + ".")
1832 # (3) Create new directory
1833 os.makedirs(newproject)
1834 # (4) Dump the tarball (if any) in the new directory
1835 if tarname:
1836 with tarfile.open(tarname, mode='r:gz') as archive:
1837 for member in archive:
1838 archive.extract(member, newproject)
emayecsb2487ae2021-08-05 10:30:13 -04001839 # (5) Copy the YAML document into the new directory. Keep the
emayecs5966a532021-07-29 10:07:02 -04001840 # original name of the project, so as to overwrite any existing
1841 # document, then change the name to match that of the project
1842 # folder.
emayecs5966a532021-07-29 10:07:02 -04001843
emayecsb2487ae2021-08-05 10:30:13 -04001844 yamlfile = newproject + '/info.yaml'
emayecs5966a532021-07-29 10:07:02 -04001845
1846 try:
emayecsb2487ae2021-08-05 10:30:13 -04001847 shutil.copy(importfile, yamlfile)
emayecs5966a532021-07-29 10:07:02 -04001848 except IOError as e:
1849 print('Error copying files: ' + str(e))
1850 return None
emayecs5966a532021-07-29 10:07:02 -04001851
1852 # (6) Remove the original files from the import folder
1853 os.remove(importfile)
1854 if tarname:
1855 os.remove(tarname)
1856
1857 # (7) Standard project setup: if spi/, elec/, and ngspice/ do not
1858 # exist, create them. If elec/.java does not exist, create it and
1859 # seed from deskel. If ngspice/run and ngspice/run/.allwaves do not
1860 # exist, create them.
1861
1862 if not os.path.exists(newproject + '/spi'):
1863 os.makedirs(newproject + '/spi')
1864 if not os.path.exists(newproject + '/spi/pex'):
1865 os.makedirs(newproject + '/spi/pex')
1866 if not os.path.exists(newproject + '/spi/lvs'):
1867 os.makedirs(newproject + '/spi/lvs')
1868 if not os.path.exists(newproject + '/ngspice'):
1869 os.makedirs(newproject + '/ngspice')
1870 if not os.path.exists(newproject + '/ngspice/run'):
1871 os.makedirs(newproject + '/ngspice/run')
1872 if not os.path.exists(newproject + '/ngspice/run/.allwaves'):
1873 os.makedirs(newproject + '/ngspice/run/.allwaves')
1874 if not os.path.exists(newproject + '/elec'):
1875 os.makedirs(newproject + '/elec')
1876 if not os.path.exists(newproject + '/xcirc'):
1877 os.makedirs(newproject + '/xcirc')
1878 if not os.path.exists(newproject + '/mag'):
1879 os.makedirs(newproject + '/mag')
1880
1881 self.reinitElec(newproject) # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any
1882
1883 return 1 # Success
1884
1885 #------------------------------------------------------------------------
1886 # Import for netlists (.spi):
1887 # (1) Request project name
1888 # (2) Create new project if name does not exist, or
1889 # place netlist in existing project if it does.
1890 #------------------------------------------------------------------------
1891
1892 #--------------------------------------------------------------------
1893 # Install netlist in electric:
1894 # "importfile" is the filename in ~/design/import
1895 # "pname" is the name of the target project (folder)
1896 # "newfile" is the netlist file name (which may or may not be the same
1897 # as 'importfile').
1898 #--------------------------------------------------------------------
1899
1900 def install_in_electric(self, importfile, pname, newfile, isnew=True):
1901 #--------------------------------------------------------------------
1902 # Install the netlist.
1903 # If netlist is CDL, then call cdl2spi first
1904 #--------------------------------------------------------------------
1905
1906 newproject = self.projectdir + '/' + pname
1907 if not os.path.isdir(newproject + '/spi/'):
1908 os.makedirs(newproject + '/spi/')
1909 if os.path.splitext(newfile)[1] == '.cdl':
1910 if not os.path.isdir(newproject + '/cdl/'):
1911 os.makedirs(newproject + '/cdl/')
1912 shutil.copy(importfile, newproject + '/cdl/' + newfile)
1913 try:
1914 p = subprocess.run(['/ef/apps/bin/cdl2spi', importfile],
1915 stdout = subprocess.PIPE, stderr = subprocess.PIPE,
1916 check = True)
1917 except subprocess.CalledProcessError as e:
1918 print('Error running cdl2spi: ' + e.output.decode('utf-8'))
1919 if isnew == True:
1920 shutil.rmtree(newproject)
1921 return None
1922 else:
1923 spi_string = p.stdout.splitlines()[0].decode('utf-8')
1924 if p.stderr:
1925 err_string = p.stderr.splitlines()[0].decode('utf-8')
1926 # Print error messages to console
1927 print(err_string)
1928 if not spi_string:
1929 print('Error: cdl2spi has no output')
1930 if isnew == True:
1931 shutil.rmtree(newproject)
1932 return None
1933 outname = os.path.splitext(newproject + '/spi/' + newfile)[0] + '.spi'
1934 with open(outname, 'w') as f:
1935 f.write(spi_string)
1936 else:
1937 outname = newproject + '/spi/' + newfile
1938 try:
1939 shutil.copy(importfile, outname)
1940 except IOError as e:
1941 print('Error copying files: ' + str(e))
1942 if isnew == True:
1943 shutil.rmtree(newproject)
1944 return None
1945
1946 #--------------------------------------------------------------------
1947 # Symbol generator---this code to be moved into its own def.
1948 #--------------------------------------------------------------------
1949 # To-do, need a more thorough SPICE parser, maybe use netgen to parse.
1950 # Need to find topmost subcircuit, by parsing the hieararchy.
1951 subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE)
1952 subnames = []
1953 with open(importfile, 'r') as f:
1954 for line in f:
1955 lmatch = subcktrex.match(line)
1956 if lmatch:
1957 subnames.append(lmatch.group(1))
1958
1959 if subnames:
1960 subname = subnames[0]
1961
1962 # Run cdl2icon perl script
1963 try:
1964 p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname',
1965 subname, '-libname', pname, '-projname', pname, '--prntgussddirs'],
1966 stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True)
1967 except subprocess.CalledProcessError as e:
1968 print('Error running cdl2spi: ' + e.output.decode('utf-8'))
1969 return None
1970 else:
1971 pin_string = p.stdout.splitlines()[0].decode('utf-8')
1972 if not pin_string:
1973 print('Error: cdl2icon has no output')
1974 if isnew == True:
1975 shutil.rmtree(newproject)
1976 return None
1977 if p.stderr:
1978 err_string = p.stderr.splitlines()[0].decode('utf-8')
1979 print(err_string)
1980
1981 # Invoke dialog to arrange pins here
1982 pin_info_list = SymbolBuilder(self, pin_string.split(), fontsize=self.prefs['fontsize']).result
1983 if not pin_info_list:
1984 # Dialog was canceled
1985 print("Symbol builder was canceled.")
1986 if isnew == True:
1987 shutil.rmtree(newproject)
1988 return 0
1989
1990 for pin in pin_info_list:
1991 pin_info = pin.split(':')
1992 pin_name = pin_info[0]
1993 pin_type = pin_info[1]
1994
1995 # Call cdl2icon with the final pin directions
1996 outname = newproject + '/elec/' + pname + '.delib/' + os.path.splitext(newfile)[0] + '.ic'
1997 try:
1998 p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname',
1999 subname, '-libname', pname, '-projname', pname, '-output',
2000 outname, '-pindircmbndstring', ','.join(pin_info_list)],
2001 stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True)
2002 except subprocess.CalledProcessError as e:
2003 print('Error running cdl2icon: ' + e.output.decode('utf-8'))
2004 if isnew == True:
2005 shutil.rmtree(newproject)
2006 return None
2007 else:
2008 icon_string = p.stdout.splitlines()[0].decode('utf-8') # not used, AFAIK
2009 if p.stderr:
2010 err_string = p.stderr.splitlines()[0].decode('utf-8')
2011 print(err_string)
2012
2013 return 1 # Success
2014
2015 #------------------------------------------------------------------------
2016 # Import netlist file into existing project
2017 #------------------------------------------------------------------------
2018
2019 def importspiceinto(self, newfile, importfile):
2020 # Require existing project location
2021 ppath = ExistingProjectDialog(self, self.get_project_list()).result
2022 if not ppath:
2023 return 0 # Canceled in dialog, no action.
2024 pname = os.path.split(ppath)[1]
2025 print("Importing into existing project " + pname)
2026 result = self.install_in_electric(importfile, pname, newfile, isnew=False)
2027 if result == None:
2028 print('Error during import.')
2029 return None
2030 elif result == 0:
2031 return 0 # Canceled
2032 else:
2033 # Remove original file from imports area
2034 os.remove(importfile)
2035 return 1 # Success
2036
2037 #------------------------------------------------------------------------
2038 # Import netlist file as a new project
2039 #------------------------------------------------------------------------
2040
2041 def importspice(self, newfile, importfile):
2042 # Use create project code first to generate a valid project space.
2043 newname = self.createproject(None)
2044 if not newname:
2045 return 0 # Canceled in dialog, no action.
2046 print("Importing as new project " + newname + ".")
2047 result = self.install_in_electric(importfile, newname, newfile, isnew=True)
2048 if result == None:
2049 print('Error during install')
2050 return None
2051 elif result == 0:
2052 # Canceled, so do not remove the import
2053 return 0
2054 else:
2055 # Remove original file from imports area
2056 os.remove(importfile)
2057 return 1 # Success
2058
2059 #------------------------------------------------------------------------
2060 # Determine if JSON's tar can be imported as-if it were just a *.v.
2061 # This is thin wrapper around tarVglImportable. Find the JSON's associated
2062 # tar.gz if any, and call tarVglImportable.
2063 # Returns list of two:
2064 # None if rules not satisified; else path of the single GL .v member.
2065 # None if rules not satisified; else root-name of the single .json member.
2066 #------------------------------------------------------------------------
2067
2068 def jsonTarVglImportable(self, path):
2069 ext = os.path.splitext(path)[1]
2070 if ext != '.json': return None, None, None
2071
2072 tar = self.json2targz(path)
2073 if not tar: return None, None, None
2074
2075 return self.tarVglImportable(tar)
emayecsb2487ae2021-08-05 10:30:13 -04002076
2077 def yamlTarVglImportable(self, path):
2078 ext = os.path.splitext(path)[1]
2079 if ext != '.yaml': return None, None, None
emayecs5966a532021-07-29 10:07:02 -04002080
emayecsb2487ae2021-08-05 10:30:13 -04002081 tar = self.yaml2targz(path)
2082 if not tar: return None, None, None
2083
2084 return self.tarVglImportable(tar)
emayecs5966a532021-07-29 10:07:02 -04002085 #------------------------------------------------------------------------
2086 # Get a single named member (memPath) out of a JSON's tar file.
2087 # This is thin wrapper around tarMember2tempfile. Find the JSON's associated
2088 # tar.gz if any, and call tarMember2tempfile.
2089 #------------------------------------------------------------------------
2090
2091 def jsonTarMember2tempfile(self, path, memPath):
2092 ext = os.path.splitext(path)[1]
2093 if ext != '.json': return None
2094
2095 tar = self.json2targz(path)
2096 if not tar: return None
2097
2098 return self.tarMember2tempfile(tar, memPath)
emayecsb2487ae2021-08-05 10:30:13 -04002099
2100 def yamlTarMember2tempfile(self, path, memPath):
2101 ext = os.path.splitext(path)[1]
2102 if ext != '.yaml': return None
2103
2104 tar = self.yaml2targz(path)
2105 if not tar: return None
2106
2107 return self.tarMember2tempfile(tar, memPath)
emayecs5966a532021-07-29 10:07:02 -04002108
2109 #------------------------------------------------------------------------
2110 # Determine if tar-file can be imported as-if it were just a *.v.
2111 # Require exactly one yosys-output .netlist.v, and exactly one .json.
2112 # Nothing else matters: Ignore all other *.v, *.tv, *.jelib, *.vcd...
2113 #
2114 # If user renames *.netlist.v in cloudv before export to not end in
2115 # netlist.v, we won't recognize it.
2116 #
2117 # Returns list of two:
2118 # None if rules not satisified; else path of the single GL netlist.v member.
2119 # None if rules not satisified; else root-name of the single .json member.
2120 #------------------------------------------------------------------------
2121
2122 def tarVglImportable(self, path):
2123 # count tar members by extensions. Track the .netlist.v. and .json. Screw the rest.
2124 nbrExt = {'.v':0, '.netlist.v':0, '.tv':0, '.jelib':0, '.json':0, '/other/':0, '/vgl/':0}
2125 nbrGLv = 0
2126 jname = None
2127 vfile = None
2128 node = None
2129 t = tarfile.open(path)
2130 for i in t:
2131 # ignore (without counting) dir entries. From cloudv (so far) the tar does not
2132 # have dir-entries, but most tar do (esp. most manually made test cases).
2133 if i.isdir():
2134 continue
2135 # TODO: should we require all below counted files to be plain files (no symlinks etc.)?
2136 # get extension, but recognize a multi-ext for .netlist.v case
2137 basenm = os.path.basename(i.name)
2138 ext = os.path.splitext(basenm)[1]
2139 root = os.path.splitext(basenm)[0]
2140 ext2 = os.path.splitext(root)[1]
2141 if ext2 == '.netlist' and ext == '.v':
2142 ext = ext2 + ext
2143 if ext and ext not in nbrExt:
2144 ext = '/other/'
2145 elif ext == '.netlist.v' and self.tarMemberIsGLverilog(t, i.name):
2146 vfile = i.name
2147 ext = '/vgl/'
2148 elif ext == '.json':
2149 node = self.tarMemberHasFoundryNode(t, i.name)
2150 jname = root
2151 nbrExt[ext] += 1
2152
2153 # check rules. Require exactly one yosys-output .netlist.v, and exactly one .json.
2154 # Quantities of other types are all don't cares.
2155 if (nbrExt['/vgl/'] == 1 and nbrExt['.json'] == 1):
2156 # vfile is the name of the verilog netlist in the tarball, while jname
2157 # is the root name of the JSON file found in the tarball (if any)
2158 return vfile, jname, node
2159
2160 # failed, not gate-level-verilog importable:
2161 return None, None, node
2162
2163
2164 #------------------------------------------------------------------------
2165 # OBSOLETE VERSION: Determine if tar-file can be imported as-if it were just a *.v.
2166 # Rules for members: one *.v, {0,1} *.jelib, {0,1} *.json, 0 other types.
2167 # Return None if rules not satisified; else return path of the single .v.
2168 #------------------------------------------------------------------------
2169 #
2170 # def tarVglImportable(self, path):
2171 # # count tar members by extensions. Track the .v.
2172 # nbrExt = {'.v':0, '.jelib':0, '.json':0, 'other':0}
2173 # vfile = ""
2174 # t = tarfile.open(path)
2175 # for i in t:
2176 # ext = os.path.splitext(i.name)[1]
2177 # if ext not in nbrExt:
2178 # ext = 'other'
2179 # nbrExt[ext] += 1
2180 # if ext == ".v": vfile = i.name
2181 #
2182 # # check rules.
2183 # if (nbrExt['.v'] != 1 or nbrExt['other'] != 0 or
2184 # nbrExt['.jelib'] > 1 or nbrExt['.json'] > 1):
2185 # return None
2186 # return vfile
2187
2188 #------------------------------------------------------------------------
2189 # Get a single named member (memPath) out of a tar file (tarPath), into a
2190 # temp-file, so subprocesses can process it.
2191 # Return path to the temp-file, or None if member not found in the tar.
2192 #------------------------------------------------------------------------
2193
2194 def tarMember2tempfile(self, tarPath, memPath):
2195 t = tarfile.open(tarPath)
2196 member = t.getmember(memPath)
2197 if not member: return None
2198
2199 # Change member.name so it extracts into our new temp-file.
2200 # extract() can specify the root-dir befow which the member path
2201 # resides. If temp is an absolute-path, that root-dir must be /.
2202 tmpf1 = tempfile.NamedTemporaryFile(delete=False)
2203 if tmpf1.name[0] != "/":
2204 raise ValueError("assertion failed, temp-file path not absolute: %s" % tmpf1.name)
2205 member.name = tmpf1.name
2206 t.extract(member,"/")
2207
2208 return tmpf1.name
2209
2210 #------------------------------------------------------------------------
2211 # Create an electric .delib directory and seed it with a header file
2212 #------------------------------------------------------------------------
2213
2214 def create_electric_header_file(self, project, libname):
2215 if not os.path.isdir(project + '/elec/' + libname + '.delib'):
2216 os.makedirs(project + '/elec/' + libname + '.delib')
2217
2218 p = subprocess.run(['electric', '-v'], stdout=subprocess.PIPE)
2219 eversion = p.stdout.splitlines()[0].decode('utf-8')
2220 # Create header file
2221 with open(project + '/elec/' + libname + '.delib/header', 'w') as f:
2222 f.write('# header information:\n')
2223 f.write('H' + libname + '|' + eversion + '\n\n')
2224 f.write('# Tools:\n')
2225 f.write('Ouser|DefaultTechnology()Sschematic\n')
2226 f.write('Osimulation|VerilogUseAssign()BT\n')
2227 f.write('C____SEARCH_FOR_CELL_FILES____\n')
2228
2229 #------------------------------------------------------------------------
2230 # Create an ad-hoc "project.json" dictionary and fill essential records
2231 #------------------------------------------------------------------------
2232
2233 def create_ad_hoc_json(self, ipname, pname):
2234 # Create ad-hoc JSON file and fill it with the minimum
2235 # necessary entries to define a project.
2236 jData = {}
2237 jDS = {}
emayecsb2487ae2021-08-05 10:30:13 -04002238 '''
emayecs5966a532021-07-29 10:07:02 -04002239 jDS['ip-name'] = ipname
emayecsb2487ae2021-08-05 10:30:13 -04002240
emayecs5966a532021-07-29 10:07:02 -04002241 pdkdir = self.get_pdk_dir(pname, path=True)
2242 try:
2243 jDS['foundry'], jDS['node'], pdk_desc, pdk_stat = self.pdkdir2fnd( pdkdir )
2244 except:
2245 # Cannot parse PDK name, so foundry and node will remain undefined
2246 pass
emayecsb2487ae2021-08-05 10:30:13 -04002247 '''
emayecs5966a532021-07-29 10:07:02 -04002248 jDS['format'] = '3'
2249 pparams = []
2250 param = {}
2251 param['unit'] = "\u00b5m\u00b2"
2252 param['condition'] = "device_area"
2253 param['display'] = "Device area"
2254 pmax = {}
2255 pmax['penalty'] = '0'
2256 pmax['target'] = '100000'
2257 param['max'] = pmax
2258 pparams.append(param)
2259
2260 param = {}
2261 param['unit'] = "\u00b5m\u00b2"
2262 param['condition'] = "area"
2263 param['display'] = "Layout area"
2264 pmax = {}
2265 pmax['penalty'] = '0'
2266 pmax['target'] = '100000'
2267 param['max'] = pmax
2268 pparams.append(param)
2269
2270 param = {}
2271 param['unit'] = "\u00b5m"
2272 param['condition'] = "width"
2273 param['display'] = "Layout width"
2274 pmax = {}
2275 pmax['penalty'] = '0'
2276 pmax['target'] = '300'
2277 param['max'] = pmax
2278 pparams.append(param)
2279
2280 param = {}
2281 param['condition'] = "DRC_errors"
2282 param['display'] = "DRC errors"
2283 pmax = {}
2284 pmax['penalty'] = 'fail'
2285 pmax['target'] = '0'
2286 param['max'] = pmax
2287 pparams.append(param)
2288
2289 param = {}
2290 param['condition'] = "LVS_errors"
2291 param['display'] = "LVS errors"
2292 pmax = {}
2293 pmax['penalty'] = 'fail'
2294 pmax['target'] = '0'
2295 param['max'] = pmax
2296 pparams.append(param)
2297
2298 jDS['physical-params'] = pparams
2299 jData['data-sheet'] = jDS
2300
2301 return jData
2302
emayecsb2487ae2021-08-05 10:30:13 -04002303#------------------------------------------------------------------------
2304 # Create info.yaml file (automatically done in create_project.py in case it's executed from the command line)
2305 #------------------------------------------------------------------------
2306
emayecs582bc382021-08-13 12:32:12 -04002307 def create_yaml(self, ipname, pdk_dir, description="(Add project description here)"):
emayecsb2487ae2021-08-05 10:30:13 -04002308 # ipname: Project Name
emayecsb2487ae2021-08-05 10:30:13 -04002309 data = {}
emayecsca79e462021-08-19 16:18:50 -04002310 project= {}
emayecsb2487ae2021-08-05 10:30:13 -04002311 project['description'] = description
2312 try:
emayecs582bc382021-08-13 12:32:12 -04002313 project['foundry'], foundry_name, project['process'], pdk_desc, pdk_stat = self.pdkdir2fnd( pdk_dir )
emayecsb2487ae2021-08-05 10:30:13 -04002314 except:
2315 # Cannot parse PDK name, so foundry and node will remain undefined
2316 pass
2317 project['project_name'] = ipname
emayecsca79e462021-08-19 16:18:50 -04002318 project['flow'] = 'none'
emayecsb2487ae2021-08-05 10:30:13 -04002319 data['project']=project
2320 return data
emayecs5966a532021-07-29 10:07:02 -04002321 #------------------------------------------------------------------------
2322 # For a single named member (memPath) out of an open tarfile (tarf),
2323 # determine if it is a JSON file, and attempt to extract value of entry
2324 # 'node' in dictionary entry 'data-sheet'. Otherwise return None.
2325 #------------------------------------------------------------------------
2326
2327 def tarMemberHasFoundryNode(self, tarf, memPath):
2328 fileJSON = tarf.extractfile(memPath)
2329 if not fileJSON: return None
2330
2331 try:
2332 # NOTE: tarfile data is in bytes, json.load(fileJSON) does not work.
2333 datatop = json.loads(fileJSON.read().decode('utf-8'))
2334 except:
2335 print("Failed to load extract file " + memPath + " as JSON data")
2336 return None
2337 else:
2338 node = None
2339 if 'data-sheet' in datatop:
2340 dsheet = datatop['data-sheet']
2341 if 'node' in dsheet:
2342 node = dsheet['node']
2343
2344 fileJSON.close() # close open-tarfile before any return
2345 return node
2346
2347 #------------------------------------------------------------------------
2348 # For a single named member (memPath) out of an open tarfile (tarf),
2349 # determine if first line embeds (case-insensitive match): Generated by Yosys
2350 # Return True or False. If no such member or it has no 1st line, returns False.
2351 #------------------------------------------------------------------------
2352
2353 def tarMemberIsGLverilog(self, tarf, memPath):
2354 fileHdl = tarf.extractfile(memPath)
2355 if not fileHdl: return False
2356
2357 line = fileHdl.readline()
2358 fileHdl.close() # close open-tarfile before any return
2359 if not line: return False
2360 return ('generated by yosys' in line.decode('utf-8').lower())
2361
2362 #------------------------------------------------------------------------
2363 # Import vgl-netlist file INTO existing project.
2364 # The importfile can be a .v; or a .json-with-tar that embeds a .v.
2365 # What is newfile? not used here.
2366 #
2367 # PROMPT to select an existing project is here.
2368 # (Is also a PROMPT to select existing electric lib, but that's within importvgl).
2369 #------------------------------------------------------------------------
2370
2371 def importvglinto(self, newfile, importfile):
2372 # Require existing project location
2373 ppath = ExistingProjectDialog(self, self.get_project_list()).result
2374 if not ppath: return 0 # Canceled in dialog, no action.
2375 pname = os.path.split(ppath)[1]
2376 print( "Importing into existing project: %s" % (pname))
2377
2378 return self.importvgl(newfile, importfile, pname)
2379
2380 #------------------------------------------------------------------------
2381 # Import cloudv project as new project.
2382 #------------------------------------------------------------------------
2383
2384 def install_from_cloudv(self, opath, ppath, pdkname, stdcellname, ydicts):
2385 oname = os.path.split(opath)[1]
2386 pname = os.path.split(ppath)[1]
2387
2388 print('Cloudv project name is ' + str(oname))
emayecs14748312021-08-05 14:21:26 -04002389 print('New project name is ' + str(pname))
emayecs5966a532021-07-29 10:07:02 -04002390
2391 os.makedirs(ppath + '/verilog', exist_ok=True)
2392
2393 vfile = None
2394 isfullchip = False
2395 ipname = oname
2396
2397 # First check for single synthesized projects, or all synthesized
2398 # digital sub-blocks within a full-chip project.
2399
2400 os.makedirs(ppath + '/verilog/source', exist_ok=True)
2401 bfiles = glob.glob(opath + '/build/*.netlist.v')
2402 for bfile in bfiles:
2403 tname = os.path.split(bfile)[1]
2404 vname = os.path.splitext(os.path.splitext(tname)[0])[0]
2405 tfile = ppath + '/verilog/' + vname + '/' + vname + '.vgl'
2406 print('Making qflow sub-project ' + vname)
2407 os.makedirs(ppath + '/verilog/' + vname, exist_ok=True)
2408 shutil.copy(bfile, tfile)
2409 if vname == oname:
2410 vfile = tfile
2411
2412 # Each build project gets its own qflow directory. Create the
2413 # source/ subdirectory and make a link back to the .vgl file.
2414 # qflow prep should do the rest.
2415
2416 os.makedirs(ppath + '/qflow', exist_ok=True)
2417 os.makedirs(ppath + '/qflow/' + vname)
2418 os.makedirs(ppath + '/qflow/' + vname + '/source')
2419
2420 # Make sure the symbolic link is relative, so that it is portable
2421 # through a shared project.
2422 curdir = os.getcwd()
2423 os.chdir(ppath + '/qflow/' + vname + '/source')
2424 os.symlink('../../../verilog/' + vname + '/' + vname + '.vgl', vname + '.v')
2425 os.chdir(curdir)
2426
2427 # Create a simple qflow_vars.sh file so that the project manager
2428 # qflow launcher will see it as a qflow sub-project. If the meta.yaml
2429 # file has a "stdcell" entry for the subproject, then add the line
2430 # "techname=" with the name of the standard cell library as pulled
2431 # from meta.yaml.
2432
2433 stdcell = None
2434 buildname = 'build/' + vname + '.netlist.v'
2435 for ydict in ydicts:
2436 if buildname in ydict:
2437 yentry = ydict[buildname]
2438 if 'stdcell' in yentry:
2439 stdcell = yentry['stdcell']
2440
2441 with open(ppath + '/qflow/' + vname + '/qflow_vars.sh', 'w') as ofile:
2442 print('#!/bin/tcsh -f', file=ofile)
2443 if stdcell:
2444 print('set techname=' + stdcell, file=ofile)
2445
2446 # Now check for a full-chip verilog SoC (from CloudV)
2447
2448 modrex = re.compile('[ \t]*module[ \t]+[^ \t(]*_?soc[ \t]*\(')
2449 genmodrex = re.compile('[ \t]*module[ \t]+([^ \t(]+)[ \t]*\(')
2450
2451 bfiles = glob.glob(opath + '/*.model/*.v')
2452 for bfile in bfiles:
2453 tname = os.path.split(bfile)[1]
2454 vpath = os.path.split(bfile)[0]
2455 ipname = os.path.splitext(tname)[0]
2456 tfile = ppath + '/verilog/' + ipname + '.v'
2457 isfullchip = True
2458 break
2459
2460 if isfullchip:
2461 print('Cloudv project IP name is ' + str(ipname))
2462
2463 # All files in */ paths should be copied to project verilog/source/,
2464 # except for the module containing the SoC itself. Note that the actual
2465 # verilog source goes here, not the synthesized netlist, although that is
2466 # mainly for efficiency of the simulation, which would normally be done in
2467 # cloudV and not in Open Galaxy. For Open Galaxy, what is needed is the
2468 # existence of a verilog file containing a module name, which is used to
2469 # track down the various files (LEF, DEF, etc.) that are needed for full-
2470 # chip layout.
2471 #
2472 # (Sept. 2019) Added copying of files in /SW/ -> /sw/ and /Verify/ ->
2473 # /verify/ for running full-chip simulations on the Open Galaxy side.
2474
2475 os.makedirs(ppath + '/verilog', exist_ok=True)
2476
2477 cfiles = glob.glob(vpath + '/source/*')
2478 for cfile in cfiles:
2479 cname = os.path.split(cfile)[1]
2480 if cname != tname:
2481 tpath = ppath + '/verilog/source/' + cname
2482 os.makedirs(ppath + '/verilog/source', exist_ok=True)
2483 shutil.copy(cfile, tpath)
2484
2485 cfiles = glob.glob(vpath + '/verify/*')
2486 for cfile in cfiles:
2487 cname = os.path.split(cfile)[1]
2488 tpath = ppath + '/verilog/verify/' + cname
2489 os.makedirs(ppath + '/verilog/verify', exist_ok=True)
2490 shutil.copy(cfile, tpath)
2491
2492 cfiles = glob.glob(vpath + '/sw/*')
2493 for cfile in cfiles:
2494 cname = os.path.split(cfile)[1]
2495 tpath = ppath + '/verilog/sw/' + cname
2496 os.makedirs(ppath + '/verilog/sw', exist_ok=True)
2497 shutil.copy(cfile, tpath)
2498
2499 # Read the top-level SoC verilog and recast it for OpenGalaxy.
2500 with open(bfile, 'r') as ifile:
2501 chiplines = ifile.read().splitlines()
2502
2503 # Find the modules used, track them down, and add the source location
2504 # in the Open Galaxy environment as an "include" line in the top level
2505 # verilog.
2506
2507 parentdir = os.path.split(bfile)[0]
2508 modfile = parentdir + '/docs/modules.txt'
2509
2510 modules = []
2511 if os.path.isfile(modfile):
2512 with open(modfile, 'r') as ifile:
2513 modules = ifile.read().splitlines()
2514 else:
2515 print("Warning: No modules.txt file for the chip top level module in "
2516 + parentdir + "/docs/.\n")
2517
2518 # Get the names of verilog libraries in this PDK.
2519 pdkdir = os.path.realpath(ppath + '/.ef-config/techdir')
2520 pdkvlog = pdkdir + '/libs.ref/verilog'
2521 pdkvlogfiles = glob.glob(pdkvlog + '/*/*.v')
2522
2523 # Read the verilog libraries and create a dictionary mapping each
2524 # module name to a location of the verilog file where it is located.
2525 moddict = {}
2526 for vlogfile in pdkvlogfiles:
2527 with open(vlogfile, 'r') as ifile:
2528 for line in ifile.read().splitlines():
2529 mmatch = genmodrex.match(line)
2530 if mmatch:
2531 modname = mmatch.group(1)
2532 moddict[modname] = vlogfile
2533
2534 # Get the names of verilog libraries in the user IP space.
2535 # (TO DO: Need to know the IP version being used!)
2536 designdir = os.path.split(ppath)[0]
2537 ipdir = designdir + '/ip/'
2538 uservlogfiles = glob.glob(ipdir + '/*/*/verilog/*.v')
2539 for vlogfile in uservlogfiles:
2540 # Strip ipdir from the front
2541 vlogpath = vlogfile.replace(ipdir, '', 1)
2542 with open(vlogfile, 'r') as ifile:
2543 for line in ifile.read().splitlines():
2544 mmatch = genmodrex.match(line)
2545 if mmatch:
2546 modname = mmatch.group(1)
2547 moddict[modname] = vlogpath
2548
2549 # Find all netlist builds from the project (those that were copied above)
2550 buildfiles = glob.glob(ppath + '/verilog/source/*.v')
2551 for vlogfile in buildfiles:
2552 # Strip ipdir from the front
2553 vlogpath = vlogfile.replace(ppath + '/verilog/source/', '', 1)
2554 with open(vlogfile, 'r') as ifile:
2555 for line in ifile.read().splitlines():
2556 mmatch = genmodrex.match(line)
2557 if mmatch:
2558 modname = mmatch.group(1)
2559 moddict[modname] = vlogpath
2560
2561 # (NOTE: removing 'ifndef LVS' as netgen should be able to handle
2562 # the contents of included files, and they are preferred since any
2563 # arrays are declared in each module I/O)
2564 # chiplines.insert(0, '`endif')
2565 chiplines.insert(0, '//--- End of list of included module dependencies ---')
2566 includedfiles = []
2567 for module in modules:
2568 # Determine where this module comes from. Look in the PDK, then in
2569 # the user ip/ directory, then in the local hierarchy. Note that
2570 # the local hierarchy expects layouts from synthesized netlists that
2571 # have not yet been created, so determine the expected location.
2572
2573 if module in moddict:
2574 if moddict[module] not in includedfiles:
2575 chiplines.insert(0, '`include "' + moddict[module] + '"')
2576 includedfiles.append(moddict[module])
2577
2578 # chiplines.insert(0, '`ifndef LVS')
2579 chiplines.insert(0, '//--- List of included module dependencies ---')
2580 chiplines.insert(0, '// iverilog simulation requires the use of -I source -I ~/design/ip')
2581 chiplines.insert(0, '// NOTE: Includes may be rooted at ~/design/ip/ or at ./source')
2582 chiplines.insert(0, '// SoC top level verilog copied and modified by project manager')
2583
2584 # Copy file, but replace the module name "soc" with the ip-name
2585 with open(tfile, 'w') as ofile:
2586 for chipline in chiplines:
2587 print(modrex.sub('module ' + ipname + ' (', chipline), file=ofile)
2588
2589 # Need to define behavior: What if there is more than one netlist?
2590 # Which one is to be imported? For now, ad-hoc behavior is to select
2591 # the last netlist file in the list if no file matches the ip-name.
2592
2593 # Note that for full-chip projects, the full chip verilog file is always
2594 # the last one set.
2595
2596 if not vfile:
2597 try:
2598 vfile = tfile
2599 except:
2600 pass
2601
2602 # NOTE: vfile was being used to create a symbol, but not any more;
2603 # see below. All the above code referencing vfile can probably be
2604 # removed.
2605
2606 try:
2607 sfiles = glob.glob(vpath + '/source/*')
2608 sfiles.extend(glob.glob(vpath + '/*/source/*'))
2609 except:
2610 sfiles = glob.glob(opath + '/*.v')
2611 sfiles.extend(glob.glob(opath + '/*.sv'))
2612 sfiles.extend(glob.glob(opath + '/local/*'))
2613
2614 for fname in sfiles:
2615 sname = os.path.split(fname)[1]
2616 tfile = ppath + '/verilog/source/' + sname
2617 # Reject '.model' and '.soc" files (these are meaningful only to CloudV)
2618 fileext = os.path.splitext(fname)[1]
2619 if fileext == '.model' or fileext == '.soc':
2620 continue
2621 if os.path.isfile(fname):
2622 # Check if /verilog/source/ has been created
2623 if not os.path.isdir(ppath + '/verilog/source'):
2624 os.makedirs(ppath + '/verilog/source')
2625 shutil.copy(fname, tfile)
2626
2627 # Add standard cell library name to project.json
2628 pjsonfile = ppath + '/project.json'
2629 if os.path.exists(pjsonfile):
2630 with open(pjsonfile, 'r') as ifile:
2631 datatop = json.load(ifile)
2632 else:
2633 datatop = self.create_ad_hoc_json(ipname, ppath)
2634
2635 # Generate a symbol in electric for the verilog top module
2636 iconfile = ppath + '/elec/' + ipname + '.delib/' + ipname + '.ic'
2637 if not os.path.exists(iconfile):
2638 # NOTE: Symbols are created by qflow migration for project
2639 # builds. Only the chip top-level needs to run create_symbol
2640 # here.
2641
2642 if isfullchip:
2643 print("Creating symbol for module " + ipname + " automatically from verilog source.")
2644 create_symbol(ppath, vfile, ipname, iconfile, False)
2645 # Add header file
2646 self.create_electric_header_file(ppath, ipname)
2647
2648 dsheet = datatop['data-sheet']
2649 if not stdcellname or stdcellname == "":
2650 dsheet['standard-cell'] = 'default'
2651 else:
2652 dsheet['standard-cell'] = stdcellname
2653
2654 with open(pjsonfile, 'w') as ofile:
2655 json.dump(datatop, ofile, indent = 4)
2656
2657 return 0
2658
2659 #------------------------------------------------------------------------
2660 # Import vgl-netlist AS new project.
2661 # The importfile can be a .v; or a .json-with-tar that embeds a .v.
2662 # What is newfile? not used here.
2663 #
2664 # PROMPT to select an create new project is within importvgl.
2665 #------------------------------------------------------------------------
2666
2667 def importvglas(self, newfile, importfile, seedname):
2668 print('importvglas: seedname is ' + str(seedname))
2669 return self.importvgl(newfile, importfile, newname=None, seedname=seedname)
2670
2671 #------------------------------------------------------------------------
2672 # Utility shared/used by both: Import vgl-netlist file AS or INTO a project.
2673 # Called directly for AS. Called via importvglinto for INTO.
2674 # importfile : source of .v to import, actual .v or json-with-tar that embeds a .v
2675 # newfile : not used
2676 # newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
2677 # Either newname is given: we PROMPT to pick an existing elecLib;
2678 # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
2679 #------------------------------------------------------------------------
2680
emayecsb2487ae2021-08-05 10:30:13 -04002681
emayecs5966a532021-07-29 10:07:02 -04002682 def importvgl(self, newfile, importfile, newname=None, seedname=None):
2683 elecLib = None
2684 isnew = not newname
2685
2686 # Up front: Determine if this import has a .json file associated
2687 # with it. If so, then parse the JSON data to find if there is a
2688 # foundry and node set for the project. If so, then the foundry
2689 # node is not selectable at time of import. Likewise, if "isnew"
2690 # is false, then we need to check if there is a directory called
2691 # "newname" and if it is set to the same foundry node. If not,
2692 # then the import must be rejected.
2693
2694 tarVfile, jName, importnode = self.jsonTarVglImportable(importfile)
2695
2696 if isnew:
2697 print('importvgl: seedname is ' + str(seedname))
2698 # Use create project code first to generate a valid project space.
2699 newname = self.createproject(None, seedname, importnode)
2700 if not newname: return 0 # Canceled in dialog, no action.
2701 print("Importing as new project " + newname + ".")
2702 elecLib = newname
2703
2704 ppath = self.projectdir + '/' + newname
2705 if not elecLib:
2706 choices = self.get_elecLib_list(newname)
2707 if not choices:
2708 print( "Aborted: No existing electric libraries found to import into.")
2709 return 0
2710
2711 elecLib = ExistingElecLibDialog(self, choices).result
2712 if not elecLib:
2713 # Never a just-created project to delete here: We only PROMPT to pick elecLib in non-new case.
2714 return 0 # Canceled in dialog, no action.
2715
2716 # Isolate just electric lib name without extension. ../a/b.delib -> b
2717 elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
2718 print("Importing to project: %s, elecLib: %s" % (newname, elecLib))
2719
2720 # Determine isolated *.v as importactual. May be importfile or tar-member (as temp-file).
2721 importactual = importfile
2722 if tarVfile:
2723 importactual = self.jsonTarMember2tempfile(importfile, tarVfile)
2724 print("importing json-with-tar's member: %s" % (tarVfile))
2725
2726 if not os.path.isfile(importactual):
2727 # TODO: should this be a raise instead?
2728 print('Error determining *.v to import')
2729 return None
2730
2731 result = self.vgl_install(importactual, newname, elecLib, newfile, isnew=isnew)
2732 if result == None:
2733 print('Error during install')
2734 return None
2735 elif result == 0:
2736 # Canceled, so do not remove the import
2737 return 0
2738 else:
2739 # If jName is non-NULL then there is a JSON file in the tarball. This is
2740 # to be used as the project JSON file. Contents of file coming from
2741 # CloudV are correct as of 12/8/2017.
2742 pname = os.path.expanduser('~/design/' + newname)
2743 legacyjname = pname + '/' + newname + '.json'
2744 # New behavior 12/2018: Project JSON file always named 'project.json'
2745 jname = pname + '/project.json'
2746
2747 # Do not overwrite an existing JSON file. Overwriting is a problem for
2748 # "import into", as the files go into an existing project, which would
2749 # normally have its own JSON file.
2750
2751 if not os.path.exists(jname) and not os.path.exists(legacyjname):
2752 try:
2753 tarJfile = os.path.split(tarVfile)[0] + '/' + jName + '.json'
2754 importjson = self.jsonTarMember2tempfile(importfile, tarJfile)
2755 except:
2756 jData = self.create_ad_hoc_json(newname, pname)
2757
2758 with open(jname, 'w') as ofile:
2759 json.dump(jData, ofile, indent = 4)
2760
2761 else:
2762 # Copy the temporary file pulled from the tarball and
2763 # remove the temporary file.
2764 shutil.copy(importjson, jname)
2765 os.remove(importjson)
2766
2767 # For time-being, if a tar.gz & json: archive them in the target project, also as extracted.
2768 # Remove original file from imports area (either .v; or .json plus tar)
2769 # plus temp-file if extracted from the tar.
2770 if importactual != importfile:
2771 os.remove(importactual)
2772 pname = self.projectdir + '/' + newname
2773 importd = pname + '/' + archiveimportdir # global: archiveimportdir
2774 os.makedirs(importd, exist_ok=True)
2775 # Dirnames to embed a VISIBLE date (UTC) of when populated.
2776 # TODO: improve dir naming or better way to store & understand later when it was processed (a log?),
2777 # without relying on file-system mtime.
2778 archived = tempfile.mkdtemp( dir=importd, prefix='{:%Y-%m-%d.%H:%M:%S}-'.format(datetime.datetime.utcnow()))
2779 tarname = self.json2targz(importfile)
2780 if tarname:
2781 with tarfile.open(tarname, mode='r:gz') as archive:
2782 for member in archive:
2783 archive.extract(member, archived)
2784 self.moveJsonPlus(importfile, archived)
2785 else:
2786 self.removeJsonPlus(importfile)
2787 return 1 # Success
2788
2789 #------------------------------------------------------------------------
2790 # Prepare multiline "warning" indicating which files to install already exist.
2791 # TODO: ugly, don't use a simple confirmation dialogue: present a proper table.
2792 #------------------------------------------------------------------------
2793 def installsConfirmMarkOverwrite(self, module, files):
2794 warning = [ "For import of module: %s," % module ]
2795 anyExists = False
2796 for i in files:
2797 exists = os.path.isfile(os.path.expanduser(i))
2798 if exists: anyExists = True
2799 warning += [ (" * " if exists else " ") + i ]
2800 if anyExists:
2801 titleSuffix = "\nCONFIRM installation of (*: OVERWRITE existing):"
2802 else:
2803 titleSuffix = "\nCONFIRM installation of:"
2804 warning[0] += titleSuffix
2805 return ConfirmInstallDialog(self, "\n".join(warning)).result
2806
2807 def vgl_install(self, importfile, pname, elecLib, newfile, isnew=True):
2808 #--------------------------------------------------------------------
2809 # Convert the in .v to: spi, cdl, elec-icon, elec-text-view forms.
2810 # TODO: Prompt to confirm final install of 5 files in dir-structure.
2811 #
2812 # newfile: argument is not used. What is it for?
2813 # Target project AND electricLib MAY BE same (pname) or different.
2814 # Rest of the filenames are determined by the module name in the source .v.
2815 #--------------------------------------------------------------------
2816
2817 newproject = self.projectdir + '/' + pname
2818 try:
2819 p = subprocess.run(['/ef/apps/bin/vglImport', importfile, pname, elecLib],
2820 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2821 check=True, universal_newlines=True)
2822 except subprocess.CalledProcessError as e:
2823 if hasattr(e, 'stdout') and e.stdout: print(e.stdout)
2824 if hasattr(e, 'stderr') and e.stderr: print(e.stderr)
2825 print('Error running vglImport: ' + str(e))
2826 if isnew == True: shutil.rmtree(newproject)
2827 return None
2828 else:
2829 dataLines = p.stdout.splitlines()
2830 if p.stderr:
2831 # Print error messages to console
2832 for i in p.stderr.splitlines(): print(i)
2833 if not dataLines or len(dataLines) != 11:
2834 print('Error: vglImport has no output, or wrong #outputs (%d vs 11)' % len(dataLines))
2835 if isnew == True: shutil.rmtree(newproject)
2836 return None
2837 else:
2838 module = dataLines[0]
2839 confirm = self.installsConfirmMarkOverwrite(module, dataLines[2::2])
2840 if not confirm:
2841 print("Cancelled")
2842 if isnew == True: shutil.rmtree(newproject)
2843 return 0
2844 # print("Proceed")
2845 clean = dataLines[1:]
2846 nbr = len(dataLines)
2847 ndx = 1
2848 # trap I/O errors and clean-up if any
2849 try:
2850 while ndx+1 < nbr:
2851 trg = os.path.expanduser(dataLines[ndx+1])
2852 os.makedirs(os.path.dirname(trg), exist_ok=True)
2853 shutil.move(dataLines[ndx], trg)
2854 ndx += 2
2855 except IOError as e:
2856 print('Error copying files: ' + str(e))
2857 for i in clean:
2858 with contextlib.suppress(FileNotFoundError): os.remove(i)
2859 if isnew == True: shutil.rmtree(newproject)
2860 return 0
2861 print( "For import of module %s installed: %s" % (module, " ".join(dataLines[2::2])))
2862 return 1 # Success
2863
2864
2865 #------------------------------------------------------------------------
2866 # Callback function from "Import Into" button on imports list box.
2867 #------------------------------------------------------------------------
2868
2869 def importintodesign(self, value):
2870 if not value['values']:
2871 print('No import selected.')
2872 return
2873
2874 # Stop the watchdog timer while this is going on
2875 self.watchclock.stop()
2876 newname = value['text']
2877 importfile = value['values'][0]
2878 print('Import project name: ' + newname + '')
2879 print('Import file name: ' + importfile + '')
2880
2881 # Behavior depends on what kind of file is being imported.
2882 # Tarballs are entire projects. Other files are individual
2883 # files and may be imported into new or existing projects
2884
2885 if os.path.isdir(importfile):
2886 print('File is a project, must import as new project.')
2887 result = self.import2project(importfile, addWarn='Redirected: A projectDir must Import-As new project.')
2888 else:
2889 ext = os.path.splitext(importfile)[1]
2890 vFile, jName, importnode = self.jsonTarVglImportable(importfile)
2891 if ((ext == '.json' and vFile) or ext == '.v'):
2892 result = self.importvglinto(newname, importfile)
2893 elif ext == '.json':
2894 # Same behavior as "Import As", at least for now
2895 print('File is a project, must import as new project.')
2896 result = self.importjson(newname, importfile)
2897 else:
2898 result = self.importspiceinto(newname, importfile)
2899
2900 if result:
2901 self.update_project_views(force=True)
2902 self.watchclock.restart()
2903
2904 #------------------------------------------------------------------------
2905 # Callback function from "Import As" button on imports list box.
2906 #------------------------------------------------------------------------
2907
2908 def importdesign(self, value):
2909 if not value['values']:
2910 print('No import selected.')
2911 return
2912
2913 # Stop the watchdog timer while this is going on
2914 self.watchclock.stop()
2915 newname = value['text']
2916 importfile = value['values'][0]
2917 print('Import project name: ' + newname)
2918 print('Import file name: ' + importfile)
2919
2920 # Behavior depends on what kind of file is being imported.
2921 # Tarballs are entire projects. Other files are individual
2922 # files and may be imported into new or existing projects
2923
2924 if os.path.isdir(importfile):
2925 result = self.import2project(importfile)
2926 else:
2927 pathext = os.path.splitext(importfile)
2928 vfile, seedname, importnode = self.jsonTarVglImportable(importfile)
2929 if ((pathext[1] == '.json' and seedname) or pathext[1] == '.v'):
2930 result = self.importvglas(newname, importfile, seedname)
2931 elif pathext[1] == '.json':
2932 result = self.importjson(newname, importfile)
2933 else:
2934 result = self.importspice(newname, importfile)
2935
2936 if result:
2937 self.update_project_views(force=True)
2938 self.watchclock.restart()
2939
2940 def deleteimport(self, value):
2941 if not value['values']:
2942 print('No import selected.')
2943 return
2944
2945 print("Delete import " + value['text'] + ' ' + value['values'][0] + " !")
2946 # Require confirmation
2947 warning = 'Confirm delete import ' + value['text'] + '?'
2948 confirm = ProtectedConfirmDialog(self, warning).result
2949 if not confirm == 'okay':
2950 return
2951 print('Delete confirmed!')
2952 item = value['values'][0]
2953
2954 if not os.path.islink(item) and os.path.isdir(item):
2955 shutil.rmtree(item)
2956 return
2957
2958 os.remove(item)
2959 ext = os.path.splitext(item)
2960 # Where import is a pair of .json and .tar.gz files, remove both.
2961 if ext[1] == '.json':
2962 if os.path.exists(ext[0] + '.tar.gz'):
2963 os.remove(ext[0] + '.tar.gz')
2964 elif os.path.exists(ext[0] + '.tgz'):
2965 os.remove(ext[0] + '.tgz')
2966
2967 def update_project_views(self, force=False):
2968 # More than updating project views, this updates projects, imports, and
2969 # IP libraries.
emayecs55354d82021-08-08 15:57:32 -04002970
emayecs5966a532021-07-29 10:07:02 -04002971 projectlist = self.get_project_list()
2972 self.projectselect.repopulate(projectlist)
2973 pdklist = self.get_pdk_list(projectlist)
2974 self.projectselect.populate2("PDK", projectlist, pdklist)
2975
emayecs12e85282021-08-11 09:37:00 -04002976 '''
emayecs5966a532021-07-29 10:07:02 -04002977 old_imports = self.number_of_imports
2978 importlist = self.get_import_list()
2979 self.importselect.repopulate(importlist)
2980 valuelist = self.importselect.getvaluelist()
2981 datelist = self.get_date_list(valuelist)
2982 itemlist = self.importselect.getlist()
2983 self.importselect.populate2("date", itemlist, datelist)
emayecs12e85282021-08-11 09:37:00 -04002984
emayecs5966a532021-07-29 10:07:02 -04002985
2986 # To do: Check if itemlist in imports changed, and open if a new import
2987 # has arrived.
2988
2989 if force or (old_imports != None) and (old_imports < self.number_of_imports):
2990 self.import_open()
2991
2992 iplist = self.get_library_list()
2993 self.ipselect.repopulate(iplist, versioning=True)
2994 valuelist = self.ipselect.getvaluelist()
2995 datelist = self.get_date_list(valuelist)
2996 itemlist = self.ipselect.getlist()
2997 self.ipselect.populate2("date", itemlist, datelist)
emayecs12e85282021-08-11 09:37:00 -04002998 '''
emayecs5966a532021-07-29 10:07:02 -04002999 def update_alert(self):
3000 # Project manager has been updated. Generate an alert window and
3001 # provide option to restart the project manager.
3002
3003 warning = 'Project manager app has been updated. Restart now?'
3004 confirm = ConfirmDialog(self, warning).result
3005 if not confirm == 'okay':
3006 print('Warning: Must quit and restart to get any fixes or updates.')
3007 return
emayecsb2487ae2021-08-05 10:30:13 -04003008 os.execl('/ef/efabless/opengalaxy/project_manager.py', 'appsel_zenity.sh')
emayecs5966a532021-07-29 10:07:02 -04003009 # Does not return; replaces existing process.
3010
3011 #----------------------------------------------------------------------
3012 # Delete a project from the design folder.
3013 #----------------------------------------------------------------------
3014
3015 def deleteproject(self, value):
3016 if not value['values']:
3017 print('No project selected.')
3018 return
emayecs55354d82021-08-08 15:57:32 -04003019 path = value['values'][0]
emayecs5966a532021-07-29 10:07:02 -04003020 print('Delete project ' + value['values'][0])
3021 # Require confirmation
3022 warning = 'Confirm delete entire project ' + value['text'] + '?'
3023 confirm = ProtectedConfirmDialog(self, warning).result
3024 if not confirm == 'okay':
3025 return
emayecs12e85282021-08-11 09:37:00 -04003026 if os.path.islink(path):
3027 os.unlink(path)
3028 self.update_project_views()
3029 else:
3030 shutil.rmtree(value['values'][0])
emayecs55354d82021-08-08 15:57:32 -04003031 if ('subcells' in path):
3032 self.update_project_views()
emayecs5966a532021-07-29 10:07:02 -04003033
3034 #----------------------------------------------------------------------
3035 # Clean out the simulation folder. Traditionally this was named
3036 # 'ngspice', so this is checked for backward-compatibility. The
3037 # proper name of the simulation directory is 'simulation'.
3038 #----------------------------------------------------------------------
3039
3040 def cleanproject(self, value):
3041 if not value['values']:
3042 print('No project selected.')
3043 return
3044 ppath = value['values'][0]
3045 print('Clean simulation raw data from directory ' + ppath)
3046 # Require confirmation
3047 warning = 'Confirm clean project ' + value['text'] + ' contents?'
3048 confirm = ConfirmDialog(self, warning).result
3049 if not confirm == 'okay':
3050 return
emayecs582bc382021-08-13 12:32:12 -04003051 else:
3052 self.clean(ppath)
3053
3054 def clean(self, ppath):
emayecs5966a532021-07-29 10:07:02 -04003055 if os.path.isdir(ppath + '/simulation'):
3056 simpath = 'simulation'
3057 elif os.path.isdir(ppath + '/ngspice'):
3058 simpath = 'ngspice'
3059 else:
3060 print('Project has no simulation folder.')
3061 return
3062
3063 filelist = os.listdir(ppath + '/' + simpath)
3064 for sfile in filelist:
3065 if os.path.splitext(sfile)[1] == '.raw':
3066 os.remove(ppath + '/ngspice/' + sfile)
3067 print('Project simulation folder cleaned.')
3068
3069 # Also clean the log file
3070 filelist = os.listdir(ppath)
3071 for sfile in filelist:
3072 if os.path.splitext(sfile)[1] == '.log':
3073 os.remove(ppath + '/' + sfile)
3074
3075 #---------------------------------------------------------------------------------------
3076 # Determine which schematic editors are compatible with the PDK, and return a list of them.
3077 #---------------------------------------------------------------------------------------
3078
3079 def list_valid_schematic_editors(self, pdktechdir):
3080 # Check PDK technology directory for xcircuit, xschem, and electric
3081 applist = []
3082 if os.path.exists(pdktechdir + '/elec'):
3083 applist.append('electric')
3084 if os.path.exists(pdktechdir + '/xschem'):
3085 applist.append('xschem')
3086 if os.path.exists(pdktechdir + '/xcircuit'):
3087 applist.append('xcircuit')
3088
3089 return applist
3090
3091 #------------------------------------------------------------------------------------------
3092 # Determine which layout editors are compatible with the PDK, and return a list of them.
3093 #------------------------------------------------------------------------------------------
3094
3095 def list_valid_layout_editors(self, pdktechdir):
3096 # Check PDK technology directory for magic and klayout
3097 applist = []
3098 if os.path.exists(pdktechdir + '/magic'):
3099 applist.append('magic')
3100 if os.path.exists(pdktechdir + '/klayout'):
3101 applist.append('klayout')
3102 return applist
3103
3104 #----------------------------------------------------------------------
3105 # Create a new project folder and initialize it (see below for steps)
3106 #----------------------------------------------------------------------
3107
3108 def createproject(self, value, seedname=None, importnode=None):
emayecs55354d82021-08-08 15:57:32 -04003109 global currdesign
emayecs5966a532021-07-29 10:07:02 -04003110 # Note: value is current selection, if any, and is ignored
3111 # Require new project location and confirmation
3112 badrex1 = re.compile("^\.")
3113 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3114 warning = 'Create new project:'
3115 print(warning)
3116 development = self.prefs['development']
emayecs55354d82021-08-08 15:57:32 -04003117
3118 # Find out whether the user wants to create a subproject or project
3119 parent_pdk = ''
3120 try:
3121 with open(os.path.expanduser(currdesign), 'r') as f:
3122 pdirCur = f.read().rstrip()
3123 if ('subcells' in pdirCur):
3124 # subproject is selected
3125 parent_path = os.path.split(os.path.split(pdirCur)[0])[0]
3126 pdkdir = self.get_pdk_dir(parent_path, path=True)
emayecs12e85282021-08-11 09:37:00 -04003127 (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( pdkdir )
emayecs55354d82021-08-08 15:57:32 -04003128 parent_pdk = foundry + '/' + node
3129 warning = 'Create new subproject in '+ parent_path + ':'
3130 elif (pdirCur[0] == '.'):
3131 # the project's 'subproject' of itself is selected
3132 parent_path = pdirCur[1:]
3133 pdkdir = self.get_pdk_dir(parent_path, path=True)
emayecs12e85282021-08-11 09:37:00 -04003134 (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( pdkdir )
emayecs55354d82021-08-08 15:57:32 -04003135 parent_pdk = foundry + '/' + node
3136 warning = 'Create new subproject in '+ parent_path + ':'
3137
3138 except:
3139 pass
3140
emayecs5966a532021-07-29 10:07:02 -04003141 while True:
3142 try:
3143 if seedname:
emayecs55354d82021-08-08 15:57:32 -04003144 newname, newpdk = NewProjectDialog(self, warning, seed=seedname, importnode=importnode, development=development, parent_pdk=parent_pdk).result
emayecs5966a532021-07-29 10:07:02 -04003145 else:
emayecs55354d82021-08-08 15:57:32 -04003146 newname, newpdk = NewProjectDialog(self, warning, seed='', importnode=importnode, development=development, parent_pdk=parent_pdk).result
emayecs5966a532021-07-29 10:07:02 -04003147 except TypeError:
3148 # TypeError occurs when "Cancel" is pressed, just handle exception.
3149 return None
3150 if not newname:
3151 return None # Canceled, no action.
emayecs55354d82021-08-08 15:57:32 -04003152
3153 if parent_pdk == '':
3154 newproject = self.projectdir + '/' + newname
3155 else:
emayecs55354d82021-08-08 15:57:32 -04003156 newproject = parent_path + '/subcells/' + newname
3157
emayecs5966a532021-07-29 10:07:02 -04003158 if self.blacklisted(newname):
3159 warning = newname + ' is not allowed for a project name.'
3160 elif badrex1.match(newname):
3161 warning = 'project name may not start with "."'
3162 elif badrex2.match(newname):
3163 warning = 'project name contains illegal characters or whitespace.'
3164 elif os.path.exists(newproject):
3165 warning = newname + ' is already a project name.'
3166 else:
3167 break
3168
emayecs12e85282021-08-11 09:37:00 -04003169 if parent_pdk !='' and not os.path.isdir(parent_path + '/subcells'):
3170 os.makedirs(parent_path + '/subcells')
3171
emayecs5966a532021-07-29 10:07:02 -04003172 try:
emayecsb2487ae2021-08-05 10:30:13 -04003173 subprocess.Popen([config.apps_path + '/create_project.py', newproject, newpdk]).wait()
emayecs5966a532021-07-29 10:07:02 -04003174
emayecs55354d82021-08-08 15:57:32 -04003175 # Show subproject in project view
3176 if parent_pdk != '':
3177 self.update_project_views()
3178
emayecs5966a532021-07-29 10:07:02 -04003179 except IOError as e:
3180 print('Error copying files: ' + str(e))
3181 return None
3182
3183 except:
3184 print('Error making project.')
3185 return None
emayecsb2487ae2021-08-05 10:30:13 -04003186
emayecs5966a532021-07-29 10:07:02 -04003187 return newname
3188 '''
3189 # Find what tools are compatible with the given PDK
3190 schemapps = self.list_valid_schematic_editors(newpdk + '/libs.tech')
3191 layoutapps = self.list_valid_layout_editors(newpdk + '/libs.tech')
3192
3193 print('New project name will be ' + newname + '.')
3194 print('Associated project PDK is ' + newpdk + '.')
3195 try:
3196 os.makedirs(newproject)
3197
3198 # Make standard folders
3199 if 'magic' in layoutapps:
3200 os.makedirs(newproject + '/mag')
3201
3202 os.makedirs(newproject + '/spi')
3203 os.makedirs(newproject + '/spi/pex')
3204 os.makedirs(newproject + '/spi/lvs')
3205 if 'electric' in layoutapps or 'electric' in schemapps:
3206 os.makedirs(newproject + '/elec')
3207 if 'xcircuit' in schemapps:
3208 os.makedirs(newproject + '/xcirc')
3209 if 'klayout' in schemapps:
3210 os.makedirs(newproject + '/klayout')
3211 os.makedirs(newproject + '/ngspice')
3212 os.makedirs(newproject + '/ngspice/run')
3213 if 'electric' in schemapps:
3214 os.makedirs(newproject + '/ngspice/run/.allwaves')
3215 os.makedirs(newproject + '/testbench')
3216 os.makedirs(newproject + '/verilog')
3217 os.makedirs(newproject + '/verilog/source')
3218 os.makedirs(newproject + '/.ef-config')
3219 if 'xschem' in schemapps:
3220 os.makedirs(newproject + '/xschem')
3221
3222 pdkname = os.path.split(newpdk)[1]
3223
3224 # Symbolic links
3225 os.symlink(newpdk, newproject + '/.ef-config/techdir')
3226
3227 # Copy preferences
3228 # deskel = '/ef/efabless/deskel'
3229 #
3230 # Copy examples (disabled; this is too confusing to the end user. Also, they
3231 # should not be in user space at all, as they are not user editable.
3232 #
3233 # for item in os.listdir(deskel + '/exlibs'):
3234 # shutil.copytree(deskel + '/exlibs/' + item, newproject + '/elec/' + item)
3235 # for item in os.listdir(deskel + '/exmag'):
3236 # if os.path.splitext(item)[1] == '.mag':
3237 # shutil.copy(deskel + '/exmag/' + item, newproject + '/mag/' + item)
3238
3239 # Put tool-specific startup files into the appropriate user directories.
3240 if 'electric' in layoutapps or 'electric' in schemapps:
3241 self.reinitElec(newproject) # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any
3242 # Set up electric
3243 self.create_electric_header_file(newproject, newname)
3244
3245 if 'magic' in layoutapps:
3246 shutil.copy(newpdk + '/libs.tech/magic/current/' + pdkname + '.magicrc', newproject + '/mag/.magicrc')
3247
3248 if 'xcircuit' in schemapps:
3249 xcircrc = newpdk + '/libs.tech/xcircuit/' + pdkname + '.' + 'xcircuitrc'
3250 xcircrc2 = newpdk + '/libs.tech/xcircuit/xcircuitrc'
3251 if os.path.exists(xcircrc):
3252 shutil.copy(xcircrc, newproject + '/xcirc/.xcircuitrc')
3253 elif os.path.exists(xcircrc2):
3254 shutil.copy(xcircrc2, newproject + '/xcirc/.xcircuitrc')
3255
3256 if 'xschem' in schemapps:
3257 xschemrc = newpdk + '/libs.tech/xschem/xschemrc'
3258 if os.path.exists(xschemrc):
3259 shutil.copy(xschemrc, newproject + '/xschem/xschemrc')
3260
3261 except IOError as e:
3262 print('Error copying files: ' + str(e))
3263 return None
3264
3265 return newname
3266 '''
3267 #----------------------------------------------------------------------
3268 # Import a CloudV project from ~/cloudv/<project_name>
3269 #----------------------------------------------------------------------
3270
3271 def cloudvimport(self, value):
3272
3273 # Require existing project location
3274 clist = self.get_cloudv_project_list()
3275 if not clist:
3276 return 0 # No projects to import
3277 ppath = ExistingProjectDialog(self, clist, warning="Enter name of cloudV project to import:").result
3278 if not ppath:
3279 return 0 # Canceled in dialog, no action.
3280 pname = os.path.split(ppath)[1]
3281 print("Importing CloudV project " + pname)
3282
3283 importnode = None
3284 stdcell = None
3285 netlistfile = None
3286
3287 # Pull process and standard cell library from the YAML file created by
3288 # CloudV. NOTE: YAML file has multiple documents, so must use
3289 # yaml.load_all(), not yaml.load(). If there are refinements of this
3290 # process for individual build files, they will override (see further down).
3291
3292 # To do: Check entries for SoC builds. If there are multiple SoC builds,
3293 # then create an additional drop-down selection to choose one, since only
3294 # one SoC build can exist as a single Open Galaxy project. Get the name
3295 # of the top-level module for the SoC. (NOTE: It may not be intended
3296 # that there can be multiple SoC builds in the project, so for now retaining
3297 # the existing parsing assuming default names.)
3298
3299 if os.path.exists(ppath + '/.ef-config/meta.yaml'):
3300 print("Reading YAML file:")
3301 ydicts = []
3302 with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
3303 yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
3304 for ydict in yalldata:
3305 ydicts.append(ydict)
3306
3307 for ydict in ydicts:
3308 for yentry in ydict.values():
3309 if 'process' in yentry:
3310 importnode = yentry['process']
3311
3312 # If there is a file ().soc and a directory ().model, then pull the file
3313 # ().model/().model.v, which is a chip top-level netlist.
3314
3315 ydicts = []
3316 has_soc = False
3317 save_vdir = None
3318 vdirs = glob.glob(ppath + '/*')
3319 for vdir in vdirs:
3320 vnameparts = vdir.split('.')
3321 if len(vnameparts) > 1 and vnameparts[-1] == 'soc' and os.path.isdir(vdir):
3322 has_soc = True
3323 if len(vnameparts) > 1 and vnameparts[-1] == 'model':
3324 save_vdir = vdir
3325
3326 if has_soc:
3327 if save_vdir:
3328 vdir = save_vdir
3329 print("INFO: CloudV project " + vdir + " is a full chip SoC project.")
3330
3331 vroot = os.path.split(vdir)[1]
3332 netlistfile = vdir + '/' + vroot + '.v'
3333 if os.path.exists(netlistfile):
3334 print("INFO: CloudV chip top level verilog is " + netlistfile + ".")
3335 else:
3336 print("ERROR: Expected SoC .model directory not found.")
3337
3338 # Otherwise, if the project has a build/ directory and a netlist.v file,
3339 # then set the foundry node accordingly.
3340
3341 elif os.path.exists(ppath + '/build'):
3342 vfiles = glob.glob(ppath + '/build/*.v')
3343 for vfile in vfiles:
3344 vroot = os.path.splitext(vfile)[0]
3345 if os.path.splitext(vroot)[1] == '.netlist':
3346 netlistfile = ppath + '/build/' + vfile
3347
3348 # Pull process and standard cell library from the YAML file
3349 # created by CloudV
3350 # Use yaml.load_all(), not yaml.load() (see above)
3351
3352 if os.path.exists(ppath + '/.ef-config/meta.yaml'):
3353 print("Reading YAML file:")
3354 ydicts = []
3355 with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
3356 yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
3357 for ydict in yalldata:
3358 ydicts.append(ydict)
3359
3360 for ydict in ydicts:
3361 for yentry in ydict.values():
3362 if 'process' in yentry:
3363 importnode = yentry['process']
3364 if 'stdcell' in yentry:
3365 stdcell = yentry['stdcell']
3366 break
3367
3368 if importnode:
3369 print("INFO: Project targets foundry process " + importnode + ".")
3370 else:
3371 print("WARNING: Project does not target any foundry process.")
3372
3373 newname = self.createproject(value, seedname=pname, importnode=importnode)
3374 if not newname: return 0 # Canceled in dialog, no action.
3375 newpath = self.projectdir + '/' + newname
3376
3377 result = self.install_from_cloudv(ppath, newpath, importnode, stdcell, ydicts)
3378 if result == None:
3379 print('Error during import.')
3380 return None
3381 elif result == 0:
3382 return 0 # Canceled
3383 else:
3384 return 1 # Success
3385
3386 #----------------------------------------------------------------------
3387 # Make a copy of a project in the design folder.
3388 #----------------------------------------------------------------------
3389
3390 def copyproject(self, value):
3391 if not value['values']:
3392 print('No project selected.')
3393 return
3394 # Require copy-to location and confirmation
3395 badrex1 = re.compile("^\.")
3396 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3397 warning = 'Copy project ' + value['text'] + ' to new project.'
3398 print('Copy project directory ' + value['values'][0])
3399 newname = ''
3400 copylist = []
3401 elprefs = False
3402 spprefs = False
3403 while True:
3404 copylist = CopyProjectDialog(self, warning, seed=newname).result
3405 if not copylist:
3406 return # Canceled, no action.
3407 else:
3408 newname = copylist[0]
3409 elprefs = copylist[1]
3410 spprefs = copylist[2]
3411 newproject = self.projectdir + '/' + newname
3412 if self.blacklisted(newname):
3413 warning = newname + ' is not allowed for a project name.'
emayecsa71088b2021-08-14 18:02:58 -04003414 elif newname == "":
3415 warning = 'Please enter a project name.'
emayecs5966a532021-07-29 10:07:02 -04003416 elif badrex1.match(newname):
3417 warning = 'project name may not start with "."'
3418 elif badrex2.match(newname):
3419 warning = 'project name contains illegal characters or whitespace.'
3420 elif os.path.exists(newproject):
3421 warning = newname + ' is already a project name.'
3422 else:
3423 break
3424
3425 oldpath = value['values'][0]
3426 oldname = os.path.split(oldpath)[1]
3427 patterns = [oldname + '.log']
3428 if not elprefs:
3429 patterns.append('.java')
3430 if not spprefs:
3431 patterns.append('ngspice')
3432 patterns.append('pv')
3433
3434 print("New project name will be " + newname)
3435 try:
emayecsa71088b2021-08-14 18:02:58 -04003436 if os.path.islink(oldpath):
3437 os.symlink(oldpath, newproject)
3438 else:
3439 shutil.copytree(oldpath, newproject, symlinks = True,
3440 ignore = shutil.ignore_patterns(*patterns))
emayecs5966a532021-07-29 10:07:02 -04003441 except IOError as e:
3442 print('Error copying files: ' + str(e))
3443 return
3444
emayecsb2487ae2021-08-05 10:30:13 -04003445 # NOTE: Behavior is for project files to depend on "project_name". Using
emayecs5966a532021-07-29 10:07:02 -04003446 # the project filename as a project name is a fallback behavior. If
emayecsb2487ae2021-08-05 10:30:13 -04003447 # there is a info.yaml file, and it defines a project_name entry, then
emayecs5966a532021-07-29 10:07:02 -04003448 # there is no need to make changes within the project. If there is
emayecsb2487ae2021-08-05 10:30:13 -04003449 # no info.yaml file, then create one and set the project_name entry to
emayecs5966a532021-07-29 10:07:02 -04003450 # the old project name, which avoids the need to make changes within
3451 # the project.
3452
3453 else:
emayecsb2487ae2021-08-05 10:30:13 -04003454 # Check info.yaml
3455 yamlname = newproject + '/info.yaml'
emayecs5966a532021-07-29 10:07:02 -04003456
3457 found = False
emayecsb2487ae2021-08-05 10:30:13 -04003458 if os.path.isfile(yamlname):
emayecs12e85282021-08-11 09:37:00 -04003459 # Pull the project_name into local store (may want to do this with the
emayecs5966a532021-07-29 10:07:02 -04003460 # datasheet as well)
emayecsb2487ae2021-08-05 10:30:13 -04003461 with open(yamlname, 'r') as f:
3462 datatop = yaml.safe_load(f)
3463 if 'project_name' in datatop['project']:
emayecs5966a532021-07-29 10:07:02 -04003464 found = True
3465
3466 if not found:
emayecsb2487ae2021-08-05 10:30:13 -04003467 pdkdir = self.get_pdk_dir(newproject, path=True)
3468 yData = self.create_yaml(oldname, pdkdir)
3469 with open(newproject + '/info.yaml', 'w') as ofile:
3470 print('---',file=ofile)
3471 yaml.dump(yData, ofile)
emayecs5966a532021-07-29 10:07:02 -04003472
3473 # If ngspice and electric prefs were not copied from the source
3474 # to the target, as recommended, then copy these from the
3475 # skeleton repository as is done when creating a new project.
3476
3477 if not spprefs:
3478 try:
3479 os.makedirs(newproject + '/ngspice')
3480 os.makedirs(newproject + '/ngspice/run')
3481 os.makedirs(newproject + '/ngspice/run/.allwaves')
3482 except FileExistsError:
3483 pass
emayecsa71088b2021-08-14 18:02:58 -04003484 '''
emayecs5966a532021-07-29 10:07:02 -04003485 if not elprefs:
3486 # Copy preferences
3487 deskel = '/ef/efabless/deskel'
3488 try:
3489 shutil.copytree(deskel + '/dotjava', newproject + '/elec/.java', symlinks = True)
3490 except IOError as e:
3491 print('Error copying files: ' + e)
emayecsa71088b2021-08-14 18:02:58 -04003492 '''
emayecs5966a532021-07-29 10:07:02 -04003493
emayecsca79e462021-08-19 16:18:50 -04003494#----------------------------------------------------------------------
3495 # Allow the user to choose the flow of the project
emayecs5966a532021-07-29 10:07:02 -04003496 #----------------------------------------------------------------------
emayecsca79e462021-08-19 16:18:50 -04003497
3498 def startflow(self, value):
3499 projectpath = value['values'][0]
3500 flow = ''
3501 warning = 'Select a flow for '+value['text']
3502 is_subproject = False
3503 try:
3504 with open(os.path.expanduser(currdesign), 'r') as f:
3505 pdirCur = f.read().rstrip()
3506 if ('subcells' in pdirCur):
3507 # subproject is selected
3508 is_subproject = True
3509 except:
3510 pass
3511 if not os.path.exists(projectpath + '/info.yaml'):
3512 project_pdkdir = self.get_pdk_dir(projectpath, path=True)
3513 data = self.create_yaml(os.path.split(projectpath)[1], project_pdkdir)
3514 with open(projectpath + '/info.yaml', 'w') as ofile:
3515 print('---',file=ofile)
3516 yaml.dump(data, ofile)
3517
3518 # Read yaml file for the selected flow
3519 with open(projectpath + '/info.yaml','r') as f:
3520 data = yaml.safe_load(f)
3521 project = data['project']
3522 if 'flow' in project.keys() and project['flow']=='none' or 'flow' not in project.keys():
3523 while True:
3524 try:
3525 flow = SelectFlowDialog(self, warning, seed='', is_subproject = is_subproject).result
3526 except TypeError:
3527 # TypeError occurs when "Cancel" is pressed, just handle exception.
3528 return None
3529 if not flow:
3530 return None # Canceled, no action.
3531 break
3532 project['flow']=flow
3533 data['project']=project
3534 with open(projectpath + '/info.yaml', 'w') as ofile:
3535 print('---',file=ofile)
3536 yaml.dump(data, ofile)
3537 else:
3538 flow = project['flow']
3539
3540 print("Starting "+flow+" flow...")
3541 if flow.lower() == 'digital':
3542 self.synthesize()
3543
3544 #----------------------------------------------------------------------
emayecs5966a532021-07-29 10:07:02 -04003545 # Change a project IP to a different name.
3546 #----------------------------------------------------------------------
3547
3548 def renameproject(self, value):
3549 if not value['values']:
3550 print('No project selected.')
3551 return
3552
3553 # Require new project name and confirmation
3554 badrex1 = re.compile("^\.")
3555 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3556 projname = value['text']
3557
emayecsb2487ae2021-08-05 10:30:13 -04003558 # Find the IP name for project projname. If it has a YAML file, then
emayecs5966a532021-07-29 10:07:02 -04003559 # read it and pull the ip-name record. If not, the fallback position
3560 # is to assume that the project filename is the project name.
3561
emayecsb2487ae2021-08-05 10:30:13 -04003562 # Check info.yaml
emayecs5966a532021-07-29 10:07:02 -04003563 projectpath = self.projectdir + '/' + projname
emayecsb2487ae2021-08-05 10:30:13 -04003564 yamlname = projectpath + '/info.yaml'
emayecs5966a532021-07-29 10:07:02 -04003565
3566 oldname = projname
emayecsb2487ae2021-08-05 10:30:13 -04003567 if os.path.isfile(yamlname):
emayecs5966a532021-07-29 10:07:02 -04003568 # Pull the ipname into local store (may want to do this with the
3569 # datasheet as well)
emayecsb2487ae2021-08-05 10:30:13 -04003570 with open(yamlname, 'r') as f:
3571 datatop = yaml.safe_load(f)
3572 project_data = datatop['project']
3573 if 'project_name' in project_data:
3574 oldname = project_data['project_name']
emayecs5966a532021-07-29 10:07:02 -04003575
3576 warning = 'Rename IP "' + oldname + '" for project ' + projname + ':'
3577 print(warning)
3578 newname = projname
3579 while True:
3580 try:
3581 newname = ProjectNameDialog(self, warning, seed=oldname + '_1').result
3582 except TypeError:
3583 # TypeError occurs when "Cancel" is pressed, just handle exception.
3584 return None
3585 if not newname:
3586 return None # Canceled, no action.
3587
3588 if self.blacklisted(newname):
3589 warning = newname + ' is not allowed for an IP name.'
3590 elif badrex1.match(newname):
3591 warning = 'IP name may not start with "."'
3592 elif badrex2.match(newname):
3593 warning = 'IP name contains illegal characters or whitespace.'
3594 else:
3595 break
3596
3597 # Update everything, including schematic, symbol, layout, JSON file, etc.
3598 print('New project IP name will be ' + newname + '.')
3599 rename_project_all(projectpath, newname)
3600
3601 # class vars: one-time compile of regulare expressions for life of the process
3602 projNameBadrex1 = re.compile("^[-.]")
3603 projNameBadrex2 = re.compile(".*[][{}()!/ \t\n\\\><#$\*\?\"'|`~]")
3604 importProjNameBadrex1 = re.compile(".*[.]bak$")
3605
3606 # centralize legal projectName check.
3607 # TODO: Several code sections are not yet converted to use this.
3608 # TODO: Extend to explain to the user the reason why.
3609 def validProjectName(self, name):
3610 return not (self.blacklisted(name) or
3611 self.projNameBadrex1.match(name) or
3612 self.projNameBadrex2.match(name))
3613
emayecs12e85282021-08-11 09:37:00 -04003614#----------------------------------------------------------------------
emayecs582bc382021-08-13 12:32:12 -04003615 # Import a project or subproject to the project manager
emayecs5966a532021-07-29 10:07:02 -04003616 #----------------------------------------------------------------------
emayecs12e85282021-08-11 09:37:00 -04003617
3618 def importproject(self, value):
3619 warning = "Import project:"
3620 badrex1 = re.compile("^\.")
3621 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3622 print(warning)
3623
emayecs582bc382021-08-13 12:32:12 -04003624 # Find out whether the user wants to import a subproject or project based on what they selected in the treeview
emayecs12e85282021-08-11 09:37:00 -04003625 parent_pdk = ''
emayecs582bc382021-08-13 12:32:12 -04003626 parent_path = ''
emayecs12e85282021-08-11 09:37:00 -04003627 try:
3628 with open(os.path.expanduser(currdesign), 'r') as f:
3629 pdirCur = f.read().rstrip()
3630 if ('subcells' in pdirCur):
3631 # subproject is selected
3632 parent_path = os.path.split(os.path.split(pdirCur)[0])[0]
3633 parent_pdkdir = self.get_pdk_dir(parent_path, path=True)
3634 (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( parent_pdkdir )
3635 parent_pdk = foundry + '/' + node
3636 warning = 'Import a subproject to '+ parent_path + ':'
3637 elif (pdirCur[0] == '.'):
3638 # the project's 'subproject' of itself is selected
3639 parent_path = pdirCur[1:]
3640 parent_pdkdir = self.get_pdk_dir(parent_path, path=True)
3641 (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( parent_pdkdir )
3642 parent_pdk = foundry + '/' + node
3643 warning = 'Import a subproject to '+ parent_path + ':'
3644
3645 except:
3646 pass
3647
3648 while True:
3649 try:
emayecs582bc382021-08-13 12:32:12 -04003650 newname, project_pdkdir, projectpath, importoption = ImportDialog(self, warning, seed='', parent_pdk = parent_pdk, parent_path = parent_path, project_dir = self.projectdir).result
emayecs12e85282021-08-11 09:37:00 -04003651 except TypeError:
3652 # TypeError occurs when "Cancel" is pressed, just handle exception.
3653 return None
3654 if not newname:
3655 return None # Canceled, no action.
3656
3657 if parent_pdk == '':
3658 newproject = self.projectdir + '/' + newname
3659 else:
3660 newproject = parent_path + '/subcells/' + newname
emayecs582bc382021-08-13 12:32:12 -04003661 break
3662
3663 def make_techdirs(projectpath, project_pdkdir):
3664 # Recursively create techdirs in project and subproject folders
emayecs12e85282021-08-11 09:37:00 -04003665 if not (os.path.exists(projectpath + '/.config') or os.path.exists(projectpath + '/.ef-config')):
3666 os.makedirs(projectpath + '/.config')
emayecs582bc382021-08-13 12:32:12 -04003667 if not os.path.exists(projectpath + self.config_path(projectpath) + '/techdir'):
3668 os.symlink(project_pdkdir, projectpath + self.config_path(projectpath) + '/techdir')
3669 if os.path.isdir(projectpath + '/subcells'):
3670 for subproject in os.listdir(projectpath + '/subcells'):
3671 subproject_path = projectpath + '/subcells/' + subproject
3672 make_techdirs(subproject_path, project_pdkdir)
3673
3674 make_techdirs(projectpath, project_pdkdir)
emayecs12e85282021-08-11 09:37:00 -04003675
emayecs582bc382021-08-13 12:32:12 -04003676 # Make symbolic link/copy projects
3677 if parent_path=='':
3678 # Create a regular project
3679 if importoption == "link":
3680 os.symlink(projectpath, self.projectdir + '/' + newname)
3681 else:
emayecs582bc382021-08-13 12:32:12 -04003682 shutil.copytree(projectpath, self.projectdir + '/' + newname, symlinks = True)
3683 if not os.path.exists(projectpath + '/info.yaml'):
3684 yData = self.create_yaml(newname, project_pdkdir)
3685 with open(projectpath + '/info.yaml', 'w') as ofile:
3686 print('---',file=ofile)
3687 yaml.dump(yData, ofile)
emayecs12e85282021-08-11 09:37:00 -04003688 else:
emayecs582bc382021-08-13 12:32:12 -04003689 #Create a subproject
emayecs12e85282021-08-11 09:37:00 -04003690 if not os.path.exists(parent_path + '/subcells'):
3691 os.makedirs(parent_path + '/subcells')
emayecs582bc382021-08-13 12:32:12 -04003692 if importoption == "copy":
emayecs582bc382021-08-13 12:32:12 -04003693 shutil.copytree(projectpath, parent_path + '/subcells/' + newname, symlinks = True)
3694 if parent_pdkdir != project_pdkdir:
3695 self.clean(parent_path + '/subcells/' + newname)
3696 else:
3697 os.symlink(projectpath, parent_path + '/subcells/' + newname)
3698 if not os.path.exists(parent_path + '/subcells/' + newname + '/info.yaml'):
3699 yData = self.create_yaml(newname, project_pdkdir)
3700 with open(parent_path + '/subcells/' + newname + '/info.yaml', 'w') as ofile:
3701 print('---',file=ofile)
3702 yaml.dump(yData, ofile)
3703 self.update_project_views()
emayecs12e85282021-08-11 09:37:00 -04003704 #----------------------------------------------------------------------
emayecs5966a532021-07-29 10:07:02 -04003705 # "Import As" a dir in import/ as a project. based on renameproject().
3706 # addWarn is used to augment confirm-dialogue if redirected here via erroneous ImportInto
3707 #----------------------------------------------------------------------
3708
3709 def import2project(self, importfile, addWarn=None):
3710 name = os.path.split(importfile)[1]
3711 projpath = self.projectdir + '/' + name
3712
3713 bakname = name + '.bak'
3714 bakpath = self.projectdir + '/' + bakname
3715 warns = []
3716 if addWarn:
3717 warns += [ addWarn ]
3718
3719 # Require new project name and confirmation
3720 confirmPrompt = None # use default: I am sure I want to do this.
3721 if os.path.isdir(projpath):
3722 if warns:
3723 warns += [ '' ] # blank line between addWarn and below two Warnings:
3724 if os.path.isdir(bakpath):
3725 warns += [ 'Warning: Replacing EXISTING: ' + name + ' AND ' + bakname + '!' ]
3726 else:
3727 warns += [ 'Warning: Replacing EXISTING: ' + name + '!' ]
3728 warns += [ 'Warning: Check for & exit any Electric,magic,qflow... for above project(s)!\n' ]
3729 confirmPrompt = 'I checked & exited apps and am sure I want to do this.'
3730
3731 warns += [ 'Confirm import-as new project: ' + name + '?' ]
3732 warning = '\n'.join(warns)
3733 confirm = ProtectedConfirmDialog(self, warning, confirmPrompt=confirmPrompt).result
3734 if not confirm == 'okay':
3735 return
3736
3737 print('New project name will be ' + name + '.')
3738 try:
3739 if os.path.isdir(projpath):
3740 if os.path.isdir(bakpath):
3741 print('Deleting old project: ' + bakpath);
3742 shutil.rmtree(bakpath)
3743 print('Moving old project ' + name + ' to ' + bakname)
3744 os.rename( projpath, bakpath)
3745 print("Importing as new project " + name)
3746 os.rename(importfile, projpath)
3747 return True
3748 except IOError as e:
3749 print("Error importing-as project: " + str(e))
3750 return None
3751
3752 #----------------------------------------------------------------------
3753 # Helper subroutine:
3754 # Check if a project is a valid project. Return the name of the
3755 # datasheet if the project has a valid one in the project top level
3756 # path.
3757 #----------------------------------------------------------------------
3758
3759 def get_datasheet_name(self, dpath):
3760 if not os.path.isdir(dpath):
3761 print('Error: Project is not a folder!')
3762 return
3763 # Check for valid datasheet name in the following order:
3764 # (1) project.json (Legacy)
3765 # (2) <name of directory>.json (Legacy)
3766 # (3) not "datasheet.json" or "datasheet_anno.json"
3767 # (4) "datasheet.json"
3768 # (5) "datasheet_anno.json"
3769
3770 dsname = os.path.split(dpath)[1]
3771 if os.path.isfile(dpath + '/project.json'):
3772 datasheet = dpath + '/project.json'
3773 elif os.path.isfile(dpath + '/' + dsname + '.json'):
3774 datasheet = dpath + '/' + dsname + '.json'
3775 else:
3776 has_generic = False
3777 has_generic_anno = False
3778 filelist = os.listdir(dpath)
3779 for file in filelist[:]:
3780 if os.path.splitext(file)[1] != '.json':
3781 filelist.remove(file)
3782 if 'datasheet.json' in filelist:
3783 has_generic = True
3784 filelist.remove('datasheet.json')
3785 if 'datasheet_anno.json' in filelist:
3786 has_generic_anno = True
3787 filelist.remove('datasheet_anno.json')
3788 if len(filelist) == 1:
3789 print('Trying ' + dpath + '/' + filelist[0])
3790 datasheet = dpath + '/' + filelist[0]
3791 elif has_generic:
3792 datasheet + dpath + '/datasheet.json'
3793 elif has_generic_anno:
3794 datasheet + dpath + '/datasheet_anno.json'
3795 else:
3796 if len(filelist) > 1:
3797 print('Error: Path ' + dpath + ' has ' + str(len(filelist)) +
3798 ' valid datasheets.')
3799 else:
3800 print('Error: Path ' + dpath + ' has no valid datasheets.')
3801 return None
3802
3803 if not os.path.isfile(datasheet):
3804 print('Error: File ' + datasheet + ' not found.')
3805 return None
3806 else:
3807 return datasheet
3808
3809 #----------------------------------------------------------------------
3810 # Run the LVS manager
3811 #----------------------------------------------------------------------
3812
3813 def run_lvs(self):
3814 value = self.projectselect.selected()
3815 if value:
3816 design = value['values'][0]
3817 # designname = value['text']
3818 designname = self.project_name
3819 print('Run LVS on design ' + designname + ' (' + design + ')')
3820 # use Popen, not run, so that application does not wait for it to exit.
emayecs5656b2b2021-08-04 12:44:13 -04003821 subprocess.Popen(['netgen','-gui',design, designname])
emayecs5966a532021-07-29 10:07:02 -04003822 else:
3823 print("You must first select a project.", file=sys.stderr)
3824
3825 #----------------------------------------------------------------------
3826 # Run the local characterization checker
3827 #----------------------------------------------------------------------
3828
3829 def characterize(self):
3830 value = self.projectselect.selected()
3831 if value:
3832 design = value['values'][0]
3833 # designname = value['text']
3834 designname = self.project_name
3835 datasheet = self.get_datasheet_name(design)
3836 print('Characterize design ' + designname + ' (' + datasheet + ' )')
3837 if datasheet:
3838 # use Popen, not run, so that application does not wait for it to exit.
3839 dsheetroot = os.path.splitext(datasheet)[0]
emayecsf31fb7a2021-08-04 16:27:55 -04003840 subprocess.Popen([config.apps_path + '/cace.py',
emayecs5966a532021-07-29 10:07:02 -04003841 datasheet])
3842 else:
3843 print("You must first select a project.", file=sys.stderr)
3844
3845 #----------------------------------------------------------------------
3846 # Run the local synthesis tool (qflow)
3847 #----------------------------------------------------------------------
3848
3849 def synthesize(self):
3850 value = self.projectselect.selected()
3851 if value:
emayecsca79e462021-08-19 16:18:50 -04003852 design = value['values'][0] # project path
3853 pdkdir = self.get_pdk_dir(design, path = True)
emayecs5966a532021-07-29 10:07:02 -04003854 # designname = value['text']
3855 designname = self.project_name
3856 development = self.prefs['devstdcells']
3857 if not designname:
3858 # A project without a datasheet has no designname (which comes from
3859 # the 'ip-name' record in the datasheet JSON) but can still be
3860 # synthesized.
3861 designname = design
3862
3863 # Normally there is one digital design in a project. However, full-chip
3864 # designs (in particular) may have multiple sub-projects that are
3865 # independently synthesized digital blocks. Find all subdirectories of
3866 # the top level or subdirectories of qflow that contain a 'qflow_vars.sh'
3867 # file. If there is more than one, then present a list. If there is
3868 # only one but it is not in 'qflow/', then be sure to pass the actual
3869 # directory name to the qflow manager.
emayecs5966a532021-07-29 10:07:02 -04003870 qvlist = glob.glob(design + '/*/qflow_vars.sh')
3871 qvlist.extend(glob.glob(design + '/qflow/*/qflow_vars.sh'))
3872 if len(qvlist) > 1 or (len(qvlist) == 1 and not os.path.exists(design + '/qflow/qflow_vars.sh')):
3873 # Generate selection menu
3874 if len(qvlist) > 1:
3875 clist = list(os.path.split(item)[0] for item in qvlist)
3876 ppath = ExistingProjectDialog(self, clist, warning="Enter name of qflow project to open:").result
3877 if not ppath:
3878 return 0 # Canceled in dialog, no action.
3879 else:
3880 ppath = os.path.split(qvlist[0])[0]
3881
3882 # pname is everything in ppath after matching design:
3883 pname = ppath.replace(design + '/', '')
3884
3885 print('Synthesize design in qflow project directory ' + pname)
emayecsca79e462021-08-19 16:18:50 -04003886 print('Loading digital flow manager...')
3887 #TODO: replace hard-coded path with function that gets the qflow manager path
emayecs5966a532021-07-29 10:07:02 -04003888 if development:
emayecsca79e462021-08-19 16:18:50 -04003889 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
emayecs5966a532021-07-29 10:07:02 -04003890 design, '-development', '-subproject=' + pname])
3891 else:
emayecsca79e462021-08-19 16:18:50 -04003892 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
emayecs5966a532021-07-29 10:07:02 -04003893 design, '-subproject=' + pname])
3894 else:
3895 print('Synthesize design ' + designname + ' (' + design + ')')
emayecsca79e462021-08-19 16:18:50 -04003896 print('Loading digital flow manager...')
emayecs5966a532021-07-29 10:07:02 -04003897 # use Popen, not run, so that application does not wait for it to exit.
3898 if development:
emayecsca79e462021-08-19 16:18:50 -04003899 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
3900 pdkdir, design, designname, '-development'])
emayecs5966a532021-07-29 10:07:02 -04003901 else:
emayecsca79e462021-08-19 16:18:50 -04003902 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
3903 pdkdir, design, designname])
emayecs5966a532021-07-29 10:07:02 -04003904 else:
3905 print("You must first select a project.", file=sys.stderr)
3906
3907 #----------------------------------------------------------------------
3908 # Switch between showing and hiding the import list (default hidden)
3909 #----------------------------------------------------------------------
3910
3911 def import_toggle(self):
3912 import_state = self.toppane.import_frame.import_header3.cget('text')
3913 if import_state == '+':
3914 self.importselect.grid(row = 11, sticky = 'news')
3915 self.toppane.import_frame.import_header3.config(text='-')
3916 else:
3917 self.importselect.grid_forget()
3918 self.toppane.import_frame.import_header3.config(text='+')
3919
3920 def import_open(self):
3921 self.importselect.grid(row = 11, sticky = 'news')
3922 self.toppane.import_frame.import_header3.config(text='-')
3923
3924 #----------------------------------------------------------------------
3925 # Switch between showing and hiding the IP library list (default hidden)
3926 #----------------------------------------------------------------------
3927
3928 def library_toggle(self):
3929 library_state = self.toppane.library_frame.library_header3.cget('text')
3930 if library_state == '+':
3931 self.ipselect.grid(row = 8, sticky = 'news')
3932 self.toppane.library_frame.library_header3.config(text='-')
3933 else:
3934 self.ipselect.grid_forget()
3935 self.toppane.library_frame.library_header3.config(text='+')
3936
3937 def library_open(self):
3938 self.ipselect.grid(row = 8, sticky = 'news')
3939 self.toppane.library_frame.library_header3.config(text='-')
3940
3941 #----------------------------------------------------------------------
3942 # Run padframe-calc (today internally invokes libreoffice, we only need cwd set to design project)
3943 #----------------------------------------------------------------------
3944 def padframe_calc(self):
3945 value = self.projectselect.selected()
3946 if value:
3947 designname = self.project_name
3948 self.padframe_calc_work(newname=designname)
3949 else:
3950 print("You must first select a project.", file=sys.stderr)
3951
3952 #------------------------------------------------------------------------
3953 # Run padframe-calc (today internally invokes libreoffice, we set cwd to design project)
3954 # Modelled somewhat after 'def importvgl':
3955 # Prompt for an existing electric lib.
3956 # Prompt for a target cellname (for both mag and electric icon).
3957 # (The AS vs INTO behavior is incomplete as yet. Used so far with current-project as newname arg).
3958 # newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
3959 # Either newname is given: we PROMPT to pick an existing elecLib;
3960 # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
3961 #------------------------------------------------------------------------
3962 def padframe_calc_work(self, newname=None):
3963 elecLib = newname
3964 isnew = not newname
3965 if isnew:
3966 # Use create project code first to generate a valid project space.
3967 newname = self.createproject(None)
3968 if not newname: return 0 # Canceled in dialog, no action.
3969 # print("padframe-calc in new project " + newname + ".")
3970 elecLib = newname
3971
3972 # For life of this projectManager process, store/recall last PadFrame Settings per project
3973 global project2pfd
3974 try:
3975 project2pfd
3976 except:
3977 project2pfd = {}
3978 if newname not in project2pfd:
3979 project2pfd[newname] = {"libEntry": None, "cellName": None}
3980
3981 ppath = self.projectdir + '/' + newname
3982 choices = self.get_elecLib_list(newname)
3983 if not choices:
3984 print( "Aborted: No existing electric libraries found to write symbol into.")
3985 return 0
3986
3987 elecLib = newname + '/elec/' + elecLib + '.delib'
3988 elecLib = project2pfd[newname]["libEntry"] or elecLib
3989 cellname = project2pfd[newname]["cellName"] or "padframe"
3990 libAndCell = ExistingElecLibCellDialog(self, None, title="PadFrame Settings", plist=choices, descPost="of icon&layout", seedLibNm=elecLib, seedCellNm=cellname).result
3991 if not libAndCell:
3992 return 0 # Canceled in dialog, no action.
3993
3994 (elecLib, cellname) = libAndCell
3995 if not cellname:
3996 return 0 # empty cellname, no action.
3997
3998 project2pfd[newname]["libEntry"] = elecLib
3999 project2pfd[newname]["cellName"] = cellname
4000
4001 # Isolate just electric lib name without extension. ../a/b.delib -> b
4002 elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
4003 print("padframe-calc in project: %s, elecLib: %s, cellName: %s" % (newname, elecLib, cellname))
4004
4005 export = dict(os.environ)
4006 export['EF_DESIGNDIR'] = ppath
4007 subprocess.Popen(['/ef/apps/bin/padframe-calc', elecLib, cellname], cwd = ppath, env = export)
4008
4009 # not yet any useful return value or reporting of results here in projectManager...
4010 return 1
4011
4012 #----------------------------------------------------------------------
4013 # Run the schematic editor (tool as given by user preference)
4014 #----------------------------------------------------------------------
4015
4016 def edit_schematic(self):
4017 value = self.projectselect.selected()
4018 if value:
4019 design = value['values'][0]
4020
4021 pdktechdir = design + self.config_path(design)+'/techdir/libs.tech'
4022
4023 applist = self.list_valid_schematic_editors(pdktechdir)
4024
4025 if len(applist)==0:
4026 print("Unable to find a valid schematic editor.")
4027 return
4028
4029 # If the preferred app is in the list, then use it.
4030
4031 if self.prefs['schemeditor'] in applist:
4032 appused = self.prefs['schemeditor']
4033 else:
4034 appused = applist[0]
4035
4036 if appused == 'xcircuit':
4037 return self.edit_schematic_with_xcircuit()
4038 elif appused == 'xschem':
4039 return self.edit_schematic_with_xschem()
4040 elif appused == 'electric':
4041 return self.edit_schematic_with_electric()
4042 else:
4043 print("Unknown/unsupported schematic editor " + appused + ".", file=sys.stderr)
4044
4045 else:
4046 print("You must first select a project.", file=sys.stderr)
4047
4048 #----------------------------------------------------------------------
4049 # Run the schematic editor (electric)
4050 #----------------------------------------------------------------------
4051
4052 def edit_schematic_with_electric(self):
4053 value = self.projectselect.selected()
4054 if value:
4055 design = value['values'][0]
4056 # designname = value['text']
4057 # self.project_name set by setcurrent. This is the true project
4058 # name, as opposed to the directory name.
4059 designname = self.project_name
4060 print('Edit schematic ' + designname + ' (' + design + ' )')
4061 # Collect libs on command-line; electric opens these in Explorer
4062 libs = []
4063 ellibrex = re.compile(r'^(tech_.*|ef_examples)\.[dj]elib$', re.IGNORECASE)
4064
4065 self.reinitElec(design)
4066
4067 # /elec and /.java are prerequisites for running electric
4068 if not os.path.exists(design + '/elec'):
4069 print("No path to electric design folder.")
4070 return
4071
4072 if not os.path.exists(design + '/elec/.java'):
4073 print("No path to electric .java folder.")
4074 return
4075
4076 # Fix the LIBDIRS file if needed
4077 #fix_libdirs(design, create = True)
4078
4079 # Check for legacy directory (missing .ef-config and/or .ef-config/techdir);
4080 # Handle as necessary.
4081
4082 # don't sometimes yield pdkdir as some subdir of techdir
4083 pdkdir = design + self.config_path(design) + '/techdir/'
4084 if not os.path.exists(pdkdir):
4085 export = dict(os.environ)
4086 export['EF_DESIGNDIR'] = design
4087 '''
4088 p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
4089 stdout = subprocess.PIPE, env = export)
4090 config_out = p.stdout.splitlines()
4091 for line in config_out:
4092 setline = line.decode('utf-8').split('=')
4093 if setline[0] == 'EF_TECHDIR':
4094 pdkdir = re.sub("[';]", "", setline[1])
4095 '''
4096
4097 for subpath in ('libs.tech/elec/', 'libs.ref/elec/'):
4098 pdkelec = os.path.join(pdkdir, subpath)
4099 if os.path.exists(pdkelec) and os.path.isdir(pdkelec):
4100 # don't use os.walk(), it is recursive, wastes time
4101 for entry in os.scandir(pdkelec):
4102 if ellibrex.match(entry.name):
4103 libs.append(entry.path)
4104
4105 # Locate most useful project-local elec-lib to open on electric cmd-line.
4106 designroot = os.path.split(design)[1]
4107 finalInDesDirLibAdded = False
4108 if os.path.exists(design + '/elec/' + designname + '.jelib'):
4109 libs.append(design + '/elec/' + designname + '.jelib')
4110 finalInDesDirLibAdded = True
4111 elif os.path.isdir(design + '/elec/' + designname + '.delib'):
4112 libs.append(design + '/elec/' + designname + '.delib')
4113 finalInDesDirLibAdded = True
4114 else:
4115 # Alternative path is the project name + .delib
4116 if os.path.isdir(design + '/elec/' + designroot + '.delib'):
4117 libs.append(design + '/elec/' + designroot + '.delib')
4118 finalInDesDirLibAdded = True
4119
4120 # Finally, check for the one absolute requirement for a project,
4121 # which is that there must be a symbol designname + .ic in the
4122 # last directory. If not, then do a search for it.
4123 if not finalInDesDirLibAdded or not os.path.isfile(libs[-1] + '/' + designname + '.ic'):
4124 delibdirs = os.listdir(design + '/elec')
4125 for delibdir in delibdirs:
4126 if os.path.splitext(delibdir)[1] == '.delib':
4127 iconfiles = os.listdir(design + '/elec/' + delibdir)
4128 for iconfile in iconfiles:
4129 if iconfile == designname + '.ic':
4130 libs.append(design + '/elec/' + delibdir)
4131 finalInDesDirLibAdded = True
4132 break
4133
4134 # Above project-local lib-adds are all conditional on finding some lib
4135 # with an expected name or content: all of which may fail.
4136 # Force last item ALWAYS to be 'a path' in the project's elec/ dir.
4137 # Usually it's a real library (found above). (If lib does not exist the messages
4138 # window does get an error message). But the purpose is for the universal side-effect:
4139 # To EVERY TIME reseed the File/OpenLibrary dialogue WorkDir to start in
4140 # project's elec/ dir; avoid it starting somewhere in the PDK, which
4141 # is what will happen if last actual cmd-line arg is a lib in the PDK, and
4142 # about which users have complained. (Optimal fix needs electric enhancement).
4143 if not finalInDesDirLibAdded:
4144 libs.append(design + '/elec/' + designroot + '.delib')
4145
4146 # Pull last item from libs and make it a command-line argument.
4147 # All other libraries become part of the EOPENARGS environment variable,
4148 # and electric is called with the elecOpen.bsh script.
4149 indirectlibs = libs[:-1]
4150 export = dict(os.environ)
4151 arguments = []
4152 if indirectlibs:
4153 export['EOPENARGS'] = ' '.join(indirectlibs)
4154 arguments.append('-s')
4155 arguments.append('/ef/efabless/lib/elec/elecOpen.bsh')
4156
4157 try:
4158 arguments.append(libs[-1])
4159 except IndexError:
4160 print('Error: Electric project directories not set up correctly?')
4161 else:
4162 subprocess.Popen(['electric', *arguments], cwd = design + '/elec',
4163 env = export)
4164 else:
4165 print("You must first select a project.", file=sys.stderr)
4166
4167 #----------------------------------------------------------------------
4168 # Run the schematic editor (xcircuit)
4169 #----------------------------------------------------------------------
4170
4171 def edit_schematic_with_xcircuit(self):
4172 value = self.projectselect.selected()
4173 if value:
4174 design = value['values'][0]
4175 # designname = value['text']
4176 # self.project_name set by setcurrent. This is the true project
4177 # name, as opposed to the directory name.
4178 designname = self.project_name
4179 print('Edit schematic ' + designname + ' (' + design + ' )')
4180 xcircdirpath = design + '/xcirc'
4181 pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xcircuit'
4182
4183 # /xcirc directory is a prerequisite for running xcircuit. If it doesn't
4184 # exist, create it and seed it with .xcircuitrc from the tech directory
4185 if not os.path.exists(xcircdirpath):
4186 os.makedirs(xcircdirpath)
4187
4188 # Copy xcircuit startup file from tech directory
4189 hasxcircrcfile = os.path.exists(xcircdirpath + '/.xcircuitrc')
4190 if not hasxcircrcfile:
4191 if os.path.exists(pdkdir + '/xcircuitrc'):
4192 shutil.copy(pdkdir + '/xcircuitrc', xcircdirpath + '/.xcircuitrc')
4193
4194 # Command line argument is the project name
4195 arguments = [design + '/xcirc' + designname]
4196 subprocess.Popen(['xcircuit', *arguments])
4197 else:
4198 print("You must first select a project.", file=sys.stderr)
4199
4200 #----------------------------------------------------------------------
4201 # Run the schematic editor (xschem)
4202 #----------------------------------------------------------------------
4203
4204 def edit_schematic_with_xschem(self):
4205 value = self.projectselect.selected()
4206 if value:
4207 design = value['values'][0]
4208 # self.project_name set by setcurrent. This is the true project
4209 # name, as opposed to the directory name.
4210 designname = self.project_name
4211 print('Edit schematic ' + designname + ' (' + design + ' )')
4212 xschemdirpath = design + '/xschem'
4213
4214 pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xschem'
4215
4216
4217 # /xschem directory is a prerequisite for running xschem. If it doesn't
4218 # exist, create it and seed it with xschemrc from the tech directory
4219 if not os.path.exists(xschemdirpath):
4220 os.makedirs(xschemdirpath)
4221
4222 # Copy xschem startup file from tech directory
4223 hasxschemrcfile = os.path.exists(xschemdirpath + '/xschemrc')
4224 if not hasxschemrcfile:
4225 if os.path.exists(pdkdir + '/xschemrc'):
4226 shutil.copy(pdkdir + '/xschemrc', xschemdirpath + '/xschemrc')
4227
4228 # Command line argument is the project name. The "-r" option is recommended if there
4229 # is no stdin/stdout piping.
4230
4231 arguments = ['-r', design + '/xschem/' + designname]
4232 subprocess.Popen(['xschem', *arguments])
4233 else:
4234 print("You must first select a project.", file=sys.stderr)
4235
4236 #----------------------------------------------------------------------
4237 # Run the layout editor (magic or klayout)
4238 #----------------------------------------------------------------------
4239
4240 def edit_layout(self):
4241 value = self.projectselect.selected()
4242 if value:
4243 design = value['values'][0]
4244 pdktechdir = design + self.config_path(design) + '/techdir/libs.tech'
4245
4246 applist = self.list_valid_layout_editors(pdktechdir)
4247
4248 if len(applist)==0:
4249 print("Unable to find a valid layout editor.")
4250 return
4251
4252 # If the preferred app is in the list, then use it.
4253 if self.prefs['layouteditor'] in applist:
4254 appused = self.prefs['layouteditor']
4255 else:
4256 appused = applist[0]
4257
4258 if appused == 'magic':
4259 return self.edit_layout_with_magic()
4260 elif appused == 'klayout':
4261 return self.edit_layout_with_klayout()
4262 elif appused == 'electric':
4263 return self.edit_layout_with_electric()
4264 else:
4265 print("Unknown/unsupported layout editor " + appused + ".", file=sys.stderr)
4266
4267 else:
4268 print("You must first select a project.", file=sys.stderr)
4269
4270 #----------------------------------------------------------------------
4271 # Run the magic layout editor
4272 #----------------------------------------------------------------------
4273
4274 def edit_layout_with_magic(self):
4275 value = self.projectselect.selected()
4276 if value:
4277 design = value['values'][0]
4278 # designname = value['text']
4279 designname = self.project_name
4280
4281 pdkdir = ''
4282 pdkname = ''
4283
4284 if os.path.exists(design + '/.ef-config/techdir/libs.tech'):
4285 pdkdir = design + '/.ef-config/techdir/libs.tech/magic/current'
4286 pdkname = os.path.split(os.path.realpath(design + '/.ef-config/techdir'))[1]
4287 elif os.path.exists(design + '/.config/techdir/libs.tech'):
4288 pdkdir = design + '/.config/techdir/libs.tech/magic'
4289 pdkname = os.path.split(os.path.realpath(design + '/.config/techdir'))[1]
4290
4291
4292 # Check if the project has a /mag directory. Create it and
4293 # put the correct .magicrc file in it, if it doesn't.
4294 magdirpath = design + '/mag'
4295 hasmagdir = os.path.exists(magdirpath)
4296 if not hasmagdir:
4297 os.makedirs(magdirpath)
4298
4299 hasmagrcfile = os.path.exists(magdirpath + '/.magicrc')
4300 if not hasmagrcfile:
4301 shutil.copy(pdkdir + '/' + pdkname + '.magicrc', magdirpath + '/.magicrc')
4302
4303 # Check if the .mag file exists for the project. If not,
4304 # generate a dialog.
4305 magpath = design + '/mag/' + designname + '.mag'
4306 netpath = design + '/spi/' + designname + '.spi'
4307 # print("magpath is " + magpath)
4308 hasmag = os.path.exists(magpath)
4309 hasnet = os.path.exists(netpath)
4310 if hasmag:
4311 if hasnet:
4312 statbuf1 = os.stat(magpath)
4313 statbuf2 = os.stat(netpath)
4314 # No specific action for out-of-date layout. To be done:
4315 # Check contents and determine if additional devices need to
4316 # be added to the layout. This may be more trouble than it's
4317 # worth.
4318 #
4319 # if statbuf2.st_mtime > statbuf1.st_mtime:
4320 # hasmag = False
4321
4322 if not hasmag:
4323 # Does the project have any .mag files at all? If so, the project
4324 # layout may be under a name different than the project name. If
4325 # so, present the user with a selectable list of layout names,
4326 # with the option to start a new layout or import from schematic.
4327
4328 maglist = os.listdir(design + '/mag/')
4329 if len(maglist) > 1:
4330 # Generate selection menu
4331 warning = 'No layout matches IP name ' + designname + '.'
4332 maglist = list(item for item in maglist if os.path.splitext(item)[1] == '.mag')
4333 clist = list(os.path.splitext(item)[0] for item in maglist)
4334 ppath = EditLayoutDialog(self, clist, ppath=design,
4335 pname=designname, warning=warning,
4336 hasnet=hasnet).result
4337 if not ppath:
4338 return 0 # Canceled in dialog, no action.
4339 elif ppath != '(New layout)':
4340 hasmag = True
4341 designname = ppath
4342 elif len(maglist) == 1:
4343 # Only one magic file, no selection, just bring it up.
4344 designname = os.path.split(maglist[0])[1]
4345 hasmag = True
4346
4347 if not hasmag:
4348 populate = NewLayoutDialog(self, "No layout for project.").result
4349 if not populate:
4350 return 0 # Canceled, no action.
4351 elif populate():
4352 # Name of PDK deprecated. The .magicrc file in the /mag directory
4353 # will load the correct PDK and specify the proper library for the
4354 # low-level device namespace, which may not be the same as techdir.
4355 # NOTE: netlist_to_layout script will attempt to generate a
4356 # schematic netlist if one does not exist.
4357
4358 print('Running /ef/efabless/bin/netlist_to_layout.py ../spi/' + designname + '.spi')
4359 try:
4360 p = subprocess.run(['/ef/efabless/bin/netlist_to_layout.py',
4361 '../spi/' + designname + '.spi'],
4362 stdin = subprocess.PIPE, stdout = subprocess.PIPE,
4363 stderr = subprocess.PIPE, cwd = design + '/mag')
4364 if p.stderr:
4365 err_string = p.stderr.splitlines()[0].decode('utf-8')
4366 # Print error messages to console
4367 print(err_string)
4368
4369 except subprocess.CalledProcessError as e:
4370 print('Error running netlist_to_layout.py: ' + e.output.decode('utf-8'))
4371 else:
4372 if os.path.exists(design + '/mag/create_script.tcl'):
4373 with open(design + '/mag/create_script.tcl', 'r') as infile:
4374 magproc = subprocess.run(['/ef/apps/bin/magic',
4375 '-dnull', '-noconsole', '-rcfile ',
4376 pdkdir + '/' + pdkname + '.magicrc', designname],
4377 stdin = infile, stdout = subprocess.PIPE,
4378 stderr = subprocess.PIPE, cwd = design + '/mag')
4379 print("Populated layout cell")
4380 # os.remove(design + '/mag/create_script.tcl')
4381 else:
4382 print("No device generating script was created.", file=sys.stderr)
4383
4384 print('Edit layout ' + designname + ' (' + design + ' )')
4385
4386 magiccommand = ['magic']
4387 # Select the graphics package used by magic from the profile settings.
4388 if 'magic-graphics' in self.prefs:
4389 magiccommand.extend(['-d', self.prefs['magic-graphics']])
4390 # Check if .magicrc predates the latest and warn if so.
4391 statbuf1 = os.stat(design + '/mag/.magicrc')
4392 statbuf2 = os.stat(pdkdir + '/' + pdkname + '.magicrc')
4393 if statbuf2.st_mtime > statbuf1.st_mtime:
4394 print('NOTE: File .magicrc predates technology startup file. Using default instead.')
4395 magiccommand.extend(['-rcfile', pdkdir + '/' + pdkname + '.magicrc'])
4396 magiccommand.append(designname)
4397
4398 # Run magic and don't wait for it to finish
4399 subprocess.Popen(magiccommand, cwd = design + '/mag')
4400 else:
4401 print("You must first select a project.", file=sys.stderr)
4402
4403 #----------------------------------------------------------------------
4404 # Run the klayout layout editor
4405 #----------------------------------------------------------------------
4406
4407 def edit_layout_with_klayout(self):
4408 value = self.projectselect.selected()
4409 print("Klayout unsupported from project manager (work in progress); run manually", file=sys.stderr)
4410
4411 #----------------------------------------------------------------------
4412 # Run the electric layout editor
4413 #----------------------------------------------------------------------
4414
4415 def edit_layout_with_electric(self):
4416 value = self.projectselect.selected()
4417 print("Electric layout editing unsupported from project manager (work in progress); run manually", file=sys.stderr)
4418
4419 #----------------------------------------------------------------------
4420 # Upload design to the marketplace
4421 # NOTE: This is not being called by anything. Use version in the
4422 # characterization script, which can check for local results before
4423 # approving (or forcing) an upload.
4424 #----------------------------------------------------------------------
4425
4426 def upload(self):
4427 '''
4428 value = self.projectselect.selected()
4429 if value:
4430 design = value['values'][0]
4431 # designname = value['text']
4432 designname = self.project_name
4433 print('Upload design ' + designname + ' (' + design + ' )')
4434 subprocess.run(['/ef/apps/bin/withnet',
emayecsb2487ae2021-08-05 10:30:13 -04004435 config.apps_path + '/cace_design_upload.py',
emayecs5966a532021-07-29 10:07:02 -04004436 design, '-test'])
4437 '''
4438
4439 #--------------------------------------------------------------------------
4440 # Upload a datasheet to the marketplace (Administrative use only, for now)
4441 #--------------------------------------------------------------------------
4442
4443 # def make_challenge(self):
4444 # importp = self.cur_import
4445 # print("Make a Challenge from import " + importp + "!")
emayecsb2487ae2021-08-05 10:30:13 -04004446 # # subprocess.run([config.apps_path + '/cace_import_upload.py', importp, '-test'])
emayecs5966a532021-07-29 10:07:02 -04004447
emayecsc707a0a2021-08-06 17:13:27 -04004448 # Runs whenever a user selects a project
emayecs5966a532021-07-29 10:07:02 -04004449 def setcurrent(self, value):
4450 global currdesign
4451 treeview = value.widget
emayecsa71088b2021-08-14 18:02:58 -04004452 selection = treeview.item(treeview.selection()) # dict with text, values, tags, etc. as keys
emayecs5966a532021-07-29 10:07:02 -04004453 pname = selection['text']
emayecsa71088b2021-08-14 18:02:58 -04004454 pdir = treeview.selection()[0] # iid of the selected project
emayecs5966a532021-07-29 10:07:02 -04004455 #print("setcurrent returned value " + pname)
emayecsc707a0a2021-08-06 17:13:27 -04004456 metapath = os.path.expanduser(currdesign)
4457 if not os.path.exists(metapath):
4458 os.makedirs(os.path.split(metapath)[0], exist_ok=True)
4459 with open(metapath, 'w') as f:
emayecs55354d82021-08-08 15:57:32 -04004460 f.write(pdir + '\n')
emayecs5966a532021-07-29 10:07:02 -04004461
4462 # Pick up the PDK from "values", use it to find the PDK folder, determine
4463 # if it has a "magic" subfolder, and enable/disable the "Edit Layout"
4464 # button accordingly
4465
4466 svalues = selection['values']
emayecsc707a0a2021-08-06 17:13:27 -04004467 #print("svalues :"+str(svalues))
emayecs5966a532021-07-29 10:07:02 -04004468 pdkitems = svalues[1].split()
4469 pdkdir = ''
4470
4471 ef_style=False
4472
4473 if os.path.exists(svalues[0] + '/.config'):
4474 pdkdir = svalues[0] + '/.config/techdir'
4475 elif os.path.exists(svalues[0] + '/.ef-config'):
4476 pdkdir = svalues[0] + '/.ef-config/techdir'
4477 ef_style=True
4478
4479 if pdkdir == '':
4480 print('No pdkname found; layout editing disabled')
4481 self.toppane.appbar.layout_button.config(state='disabled')
4482 else:
4483 try:
4484 if ef_style:
4485 subf = os.listdir(pdkdir + '/libs.tech/magic/current')
4486 else:
4487 subf = os.listdir(pdkdir + '/libs.tech/magic')
4488 except:
4489 print('PDK ' + pdkname + ' has no layout setup; layout editing disabled')
4490 self.toppane.appbar.layout_button.config(state='disabled')
emayecsc707a0a2021-08-06 17:13:27 -04004491
emayecs5966a532021-07-29 10:07:02 -04004492 # If the selected project directory has a JSON file and netlists in the "spi"
4493 # and "testbench" folders, then enable the "Characterize" button; else disable
4494 # it.
4495 # NOTE: project.json is the preferred name for the datasheet
4496 # file. However, the .spi file, .delib file, etc., all have the name of the
emayecsb2487ae2021-08-05 10:30:13 -04004497 # project from "project_name" in the info.yaml file, which is separate from the datasheet.
emayecs5966a532021-07-29 10:07:02 -04004498
4499 found = False
4500 ppath = selection['values'][0]
emayecsb2487ae2021-08-05 10:30:13 -04004501 yamlname = ppath + '/info.yaml'
4502
4503 if os.path.isfile(yamlname):
4504 # Pull the project_name into local store
4505 with open(yamlname, 'r') as f:
4506 datatop = yaml.safe_load(f)
4507 project_data = datatop['project']
4508 ipname = project_data['project_name']
4509 self.project_name = ipname
4510 else:
4511 print('Setting project ip-name from the project folder name.')
4512 self.project_name = pname
emayecs5966a532021-07-29 10:07:02 -04004513 jsonname = ppath + '/project.json'
emayecs5966a532021-07-29 10:07:02 -04004514 if os.path.isfile(jsonname):
emayecs5966a532021-07-29 10:07:02 -04004515 with open(jsonname, 'r') as f:
4516 datatop = json.load(f)
4517 dsheet = datatop['data-sheet']
emayecs5966a532021-07-29 10:07:02 -04004518 found = True
emayecs5966a532021-07-29 10:07:02 -04004519 # Do not specifically prohibit opening the characterization app if
4520 # there is no schematic or netlist. Otherwise the user is prevented
4521 # even from seeing the electrical parameters. Let the characterization
4522 # tool allow or prohibit simulation based on this.
4523 # if os.path.exists(ppath + '/spi'):
4524 # if os.path.isfile(ppath + '/spi/' + ipname + '.spi'):
4525 # found = True
4526 #
4527 # if found == False and os.path.exists(ppath + '/elec'):
4528 # if os.path.isdir(ppath + '/elec/' + ipname + '.delib'):
4529 # if os.path.isfile(ppath + '/elec/' + ipname + '.delib/' + ipname + '.sch'):
4530 # found = True
4531 else:
4532 # Use 'pname' as the default project name.
4533 print('No characterization file ' + jsonname)
emayecs5966a532021-07-29 10:07:02 -04004534
4535 # If datasheet has physical parameters but not electrical parameters, then it's okay
4536 # for it not to have a testbench directory; it's still valid. However, having
4537 # neither physical nor electrical parameters means there's nothing to characterize.
4538 if found and 'electrical-params' in dsheet and len(dsheet['electrical-params']) > 0:
4539 if not os.path.isdir(ppath + '/testbench'):
4540 print('No testbench directory for eletrical parameter simulation methods.', file=sys.stderr)
4541 found = False
4542 elif found and not 'physical-params' in dsheet:
4543 print('Characterization file defines no characterization tests.', file=sys.stderr)
4544 found = False
4545 elif found and 'physical-params' in dsheet and len(dsheet['physical-params']) == 0:
4546 print('Characterization file defines no characterization tests.', file=sys.stderr)
4547 found = False
4548
4549 if found == True:
4550 self.toppane.appbar.char_button.config(state='enabled')
4551 else:
4552 self.toppane.appbar.char_button.config(state='disabled')
4553
4554 # Warning: temporary hack (Tim, 1/9/2018)
4555 # Pad frame generator is currently limited to the XH035 cells, so if the
4556 # project PDK is not XH035, disable the pad frame button
4557
4558 if len(pdkitems) > 1 and pdkitems[1] == 'EFXH035B':
4559 self.toppane.appbar.padframeCalc_button.config(state='enabled')
4560 else:
4561 self.toppane.appbar.padframeCalc_button.config(state='disabled')
4562
4563# main app. fyi: there's a 2nd/earlier __main__ section for splashscreen
4564if __name__ == '__main__':
emayecs582bc382021-08-13 12:32:12 -04004565 ProjectManager(root)
emayecs5966a532021-07-29 10:07:02 -04004566 if deferLoad:
4567 # Without this, mainloop may find&run very short clock-delayed events BEFORE main form display.
4568 # With it 1st project-load can be scheduled using after-time=0 (needn't tune a delay like 100ms).
4569 root.update_idletasks()
4570 root.mainloop()