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