blob: 245eeee1d74517528e3402de851930d03dddcb5c [file] [log] [blame]
emayecsbf54c5b2021-08-26 14:20:46 -04001#!/usr/bin/env -S python3 -B
emayecs5966a532021-07-29 10:07:02 -04002#
3#--------------------------------------------------------
4# Open Galaxy Project Manager GUI.
5#
6# This is a Python tkinter script that handles local
7# project management. It is meant as a replacement for
8# appsel_zenity.sh
9#
10#--------------------------------------------------------
11# Written by Tim Edwards
12# efabless, inc.
13# September 9, 2016
14# Modifications 2017, 2018
15# Version 1.0
16#--------------------------------------------------------
17
18import sys
19# Require python 3.5.x (and not python 3.6.x). Without this trap here, in several
20# instances of VMs where /usr/bin/python3 symlinked to 3.6.x by mistake, it manifests
21# as (misleading) errors like: ImportError: No module named 'yaml'
22#
23# '%x' % sys.hexversion -> '30502f0'
24
25import tkinter
26from tkinter import ttk, StringVar, Listbox, END
27from tkinter import filedialog
28
29# globals
30theProg = sys.argv[0]
31root = tkinter.Tk() # WARNING: must be exactly one instance of Tk; don't call again elsewhere
32
33# 4 configurations based on booleans: splash,defer
34# n,n: no splash, show only form when completed: LEGACY MODE, user confused by visual lag.
35# n,y: no splash but defer projLoad: show an empty form ASAP
36# y,n: yes splash, and wait for projLoad before showing completed form
37# y,y: yes splash, but also defer projLoad: show empty form ASAP
38
39# deferLoad = False # LEGACY: no splash, and wait for completed form
40# doSplash = False
41
42deferLoad = True # True: display GUI before (slow) loading of projects, so no splash:
43doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects
44
45# deferLoad = False # load projects before showing form, so need splash:
46# doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects
47
48# deferLoad = True # here keep splash also, despite also deferred-loading
49# doSplash = True
50
51#------------------------------------------------------
52# Splash screen: display ASAP: BEFORE bulk of imports.
53#------------------------------------------------------
54
55class SplashScreen(tkinter.Toplevel):
emayecs14748312021-08-05 14:21:26 -040056 """Project Management Splash Screen"""
emayecs5966a532021-07-29 10:07:02 -040057
58 def __init__(self, parent, *args, **kwargs):
59 super().__init__(parent, *args, **kwargs)
60 parent.withdraw()
61 #EFABLESS PLATFORM
62 #image = tkinter.PhotoImage(file="/ef/efabless/opengalaxy/og_splashscreen50.gif")
63 label = ttk.Label(self, image=image)
64 label.pack()
65
66 # required to make window show before the program gets to the mainloop
67 self.update_idletasks()
68
69import faulthandler
70import signal
71
72# SplashScreen here. fyi: there's a 2nd/later __main__ section for main app
73splash = None # a global
74if __name__ == '__main__':
75 faulthandler.register(signal.SIGUSR2)
76 if doSplash:
77 splash = SplashScreen(root)
78
79import io
80import os
81import re
82import json
83import yaml
84import shutil
85import tarfile
86import datetime
87import subprocess
88import contextlib
89import tempfile
90import glob
91
92import tksimpledialog
93import tooltip
94from rename_project import rename_project_all
95#from fix_libdirs import fix_libdirs
96from consoletext import ConsoleText
97from helpwindow import HelpWindow
98from treeviewchoice import TreeViewChoice
99from symbolbuilder import SymbolBuilder
100from make_icon_from_soft import create_symbol
101from profile import Profile
102
emayecsf31fb7a2021-08-04 16:27:55 -0400103import config
emayecs5966a532021-07-29 10:07:02 -0400104
105# Global name for design directory
106designdir = 'design'
107# Global name for import directory
108importdir = 'import'
109# Global name for cloudv directory
110cloudvdir = 'cloudv'
111# Global name for archived imports project sub-directory
112archiveimportdir = 'imported'
113# Global name for current design file
114#EFABLESS PLATFORM
115currdesign = '~/.open_pdks/currdesign'
116prefsfile = '~/.open_pdks/prefs.json'
117
118
119#---------------------------------------------------------------
120# Watch a directory for modified time change. Repeat every two
121# seconds. Call routine callback() if a change occurs
122#---------------------------------------------------------------
123
124class WatchClock(object):
125 def __init__(self, parent, path, callback, interval=2000, interval0=None):
126 self.parent = parent
127 self.callback = callback
128 self.path = path
129 self.interval = interval
130 if interval0 != None:
131 self.interval0 = interval0
132 self.restart(first=True)
133 else:
134 self.interval0 = interval
135 self.restart()
136
137 def query(self):
138 for entry in self.path:
139 statbuf = os.stat(entry)
140 if statbuf.st_mtime > self.reftime:
141 self.callback()
142 self.restart()
143 return
144 self.timer = self.parent.after(self.interval, self.query)
145
146 def stop(self):
147 self.parent.after_cancel(self.timer)
148
149 # if first: optionally use different (typically shorter) interval, AND DON'T
150 # pre-record watched-dir mtime-s (which forces the callback on first timer fire)
151 def restart(self, first=False):
152 self.reftime = 0
153 if not first:
154 for entry in self.path:
155 statbuf = os.stat(entry)
156 if statbuf.st_mtime > self.reftime:
157 self.reftime = statbuf.st_mtime
158 self.timer = self.parent.after(self.interval0 if first and self.interval0 != None else self.interval, self.query)
159
160#------------------------------------------------------
161# Dialog for generating a new layout
162#------------------------------------------------------
163
164class NewLayoutDialog(tksimpledialog.Dialog):
165 def body(self, master, warning, seed=''):
166 if warning:
167 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
168
169 self.l1prefs = tkinter.IntVar(master)
170 self.l1prefs.set(1)
171 ttk.Checkbutton(master, text='Populate new layout from netlist',
172 variable = self.l1prefs).grid(row = 2, columnspan = 2, sticky = 'enws')
173
174 return self
175
176 def apply(self):
177 return self.l1prefs.get
178
179#------------------------------------------------------
180# Simple dialog for entering project names
181#------------------------------------------------------
182
183class ProjectNameDialog(tksimpledialog.Dialog):
184 def body(self, master, warning, seed=''):
185 if warning:
186 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
187 ttk.Label(master, text='Enter new project name:').grid(row = 1, column = 0, sticky = 'wns')
188 self.nentry = ttk.Entry(master)
189 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
190 self.nentry.insert(0, seed)
191 return self.nentry # initial focus
192
193 def apply(self):
194 return self.nentry.get()
195
196class PadFrameCellNameDialog(tksimpledialog.Dialog):
197 def body(self, master, warning, seed=''):
198 description='PadFrame' # TODO: make this an extra optional parameter of a generic CellNameDialog?
199 if warning:
200 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
201 if description:
202 description = description + " "
203 else:
204 description = ""
205 ttk.Label(master, text=("Enter %scell name:" %(description))).grid(row = 1, column = 0, sticky = 'wns')
206 self.nentry = ttk.Entry(master)
207 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
208 self.nentry.insert(0, seed)
209 return self.nentry # initial focus
210
211 def apply(self):
212 return self.nentry.get()
213
214#------------------------------------------------------
215# Dialog for copying projects. Includes checkbox
216# entries for preferences.
217#------------------------------------------------------
218
219class CopyProjectDialog(tksimpledialog.Dialog):
220 def body(self, master, warning, seed=''):
221 if warning:
222 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
223 ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0, sticky = 'wns')
224 self.nentry = ttk.Entry(master)
225 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
226 self.nentry.insert(0, seed)
227 self.elprefs = tkinter.IntVar(master)
228 self.elprefs.set(0)
229 ttk.Checkbutton(master, text='Copy electric preferences (not recommended)',
230 variable = self.elprefs).grid(row = 2, columnspan = 2, sticky = 'enws')
231 self.spprefs = tkinter.IntVar(master)
232 self.spprefs.set(0)
233 ttk.Checkbutton(master, text='Copy ngspice folder (not recommended)',
234 variable = self.spprefs).grid(row = 3, columnspan = 2, sticky = 'enws')
235 return self.nentry # initial focus
236
237 def apply(self):
238 # Return a list containing the entry text and the checkbox states.
239 elprefs = True if self.elprefs.get() == 1 else False
240 spprefs = True if self.spprefs.get() == 1 else False
241 return [self.nentry.get(), elprefs, spprefs]
242
243#-------------------------------------------------------
244# Not-Quite-So-Simple dialog for entering a new project.
245# Select a project name and a PDK from a drop-down list.
246#-------------------------------------------------------
247
248class NewProjectDialog(tksimpledialog.Dialog):
emayecs55354d82021-08-08 15:57:32 -0400249 def body(self, master, warning, seed='', importnode=None, development=False, parent_pdk=''):
emayecs5966a532021-07-29 10:07:02 -0400250 if warning:
251 ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
252 ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0)
253 self.nentry = ttk.Entry(master)
254 self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
255 self.nentry.insert(0, seed or '') # may be None
256 self.pvar = tkinter.StringVar(master)
257 if not importnode:
258 # Add PDKs as found by searching /ef/tech for 'libs.tech' directories
259 ttk.Label(master, text="Select foundry/node:").grid(row = 2, column = 0)
260 else:
261 ttk.Label(master, text="Foundry/node:").grid(row = 2, column = 0)
262 self.infolabel = ttk.Label(master, text="", style = 'brown.TLabel', wraplength=250)
263 self.infolabel.grid(row = 3, column = 0, columnspan = 2, sticky = 'news')
264 self.pdkmap = {}
265 self.pdkdesc = {}
266 self.pdkstat = {}
267 pdk_def = None
268
269 node_def = importnode
270 if not node_def:
271 node_def = "EFXH035B"
272
273 # use glob instead of os.walk. Don't need to recurse large PDK hier.
274 # TODO: stop hardwired default EFXH035B: get from an overall flow /ef/tech/.ef-config/plist.json
275 # (or get it from the currently selected project)
276 #EFABLESS PLATFORM
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:
3245 shutil.copy(newpdk + '/libs.tech/magic/current/' + pdkname + '.magicrc', newproject + '/mag/.magicrc')
3246
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')
3259
3260 except IOError as e:
3261 print('Error copying files: ' + str(e))
3262 return None
3263
3264 return newname
3265 '''
3266 #----------------------------------------------------------------------
3267 # Import a CloudV project from ~/cloudv/<project_name>
3268 #----------------------------------------------------------------------
3269
3270 def cloudvimport(self, value):
3271
3272 # Require existing project location
3273 clist = self.get_cloudv_project_list()
3274 if not clist:
3275 return 0 # No projects to import
3276 ppath = ExistingProjectDialog(self, clist, warning="Enter name of cloudV project to import:").result
3277 if not ppath:
3278 return 0 # Canceled in dialog, no action.
3279 pname = os.path.split(ppath)[1]
3280 print("Importing CloudV project " + pname)
3281
3282 importnode = None
3283 stdcell = None
3284 netlistfile = None
3285
3286 # Pull process and standard cell library from the YAML file created by
3287 # CloudV. NOTE: YAML file has multiple documents, so must use
3288 # yaml.load_all(), not yaml.load(). If there are refinements of this
3289 # process for individual build files, they will override (see further down).
3290
3291 # To do: Check entries for SoC builds. If there are multiple SoC builds,
3292 # then create an additional drop-down selection to choose one, since only
3293 # one SoC build can exist as a single Open Galaxy project. Get the name
3294 # of the top-level module for the SoC. (NOTE: It may not be intended
3295 # that there can be multiple SoC builds in the project, so for now retaining
3296 # the existing parsing assuming default names.)
3297
3298 if os.path.exists(ppath + '/.ef-config/meta.yaml'):
3299 print("Reading YAML file:")
3300 ydicts = []
3301 with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
3302 yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
3303 for ydict in yalldata:
3304 ydicts.append(ydict)
3305
3306 for ydict in ydicts:
3307 for yentry in ydict.values():
3308 if 'process' in yentry:
3309 importnode = yentry['process']
3310
3311 # If there is a file ().soc and a directory ().model, then pull the file
3312 # ().model/().model.v, which is a chip top-level netlist.
3313
3314 ydicts = []
3315 has_soc = False
3316 save_vdir = None
3317 vdirs = glob.glob(ppath + '/*')
3318 for vdir in vdirs:
3319 vnameparts = vdir.split('.')
3320 if len(vnameparts) > 1 and vnameparts[-1] == 'soc' and os.path.isdir(vdir):
3321 has_soc = True
3322 if len(vnameparts) > 1 and vnameparts[-1] == 'model':
3323 save_vdir = vdir
3324
3325 if has_soc:
3326 if save_vdir:
3327 vdir = save_vdir
3328 print("INFO: CloudV project " + vdir + " is a full chip SoC project.")
3329
3330 vroot = os.path.split(vdir)[1]
3331 netlistfile = vdir + '/' + vroot + '.v'
3332 if os.path.exists(netlistfile):
3333 print("INFO: CloudV chip top level verilog is " + netlistfile + ".")
3334 else:
3335 print("ERROR: Expected SoC .model directory not found.")
3336
3337 # Otherwise, if the project has a build/ directory and a netlist.v file,
3338 # then set the foundry node accordingly.
3339
3340 elif os.path.exists(ppath + '/build'):
3341 vfiles = glob.glob(ppath + '/build/*.v')
3342 for vfile in vfiles:
3343 vroot = os.path.splitext(vfile)[0]
3344 if os.path.splitext(vroot)[1] == '.netlist':
3345 netlistfile = ppath + '/build/' + vfile
3346
3347 # Pull process and standard cell library from the YAML file
3348 # created by CloudV
3349 # Use yaml.load_all(), not yaml.load() (see above)
3350
3351 if os.path.exists(ppath + '/.ef-config/meta.yaml'):
3352 print("Reading YAML file:")
3353 ydicts = []
3354 with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
3355 yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
3356 for ydict in yalldata:
3357 ydicts.append(ydict)
3358
3359 for ydict in ydicts:
3360 for yentry in ydict.values():
3361 if 'process' in yentry:
3362 importnode = yentry['process']
3363 if 'stdcell' in yentry:
3364 stdcell = yentry['stdcell']
3365 break
3366
3367 if importnode:
3368 print("INFO: Project targets foundry process " + importnode + ".")
3369 else:
3370 print("WARNING: Project does not target any foundry process.")
3371
3372 newname = self.createproject(value, seedname=pname, importnode=importnode)
3373 if not newname: return 0 # Canceled in dialog, no action.
3374 newpath = self.projectdir + '/' + newname
3375
3376 result = self.install_from_cloudv(ppath, newpath, importnode, stdcell, ydicts)
3377 if result == None:
3378 print('Error during import.')
3379 return None
3380 elif result == 0:
3381 return 0 # Canceled
3382 else:
3383 return 1 # Success
3384
3385 #----------------------------------------------------------------------
3386 # Make a copy of a project in the design folder.
3387 #----------------------------------------------------------------------
3388
3389 def copyproject(self, value):
3390 if not value['values']:
3391 print('No project selected.')
3392 return
3393 # Require copy-to location and confirmation
3394 badrex1 = re.compile("^\.")
3395 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3396 warning = 'Copy project ' + value['text'] + ' to new project.'
3397 print('Copy project directory ' + value['values'][0])
3398 newname = ''
3399 copylist = []
3400 elprefs = False
3401 spprefs = False
3402 while True:
3403 copylist = CopyProjectDialog(self, warning, seed=newname).result
3404 if not copylist:
3405 return # Canceled, no action.
3406 else:
3407 newname = copylist[0]
3408 elprefs = copylist[1]
3409 spprefs = copylist[2]
3410 newproject = self.projectdir + '/' + newname
3411 if self.blacklisted(newname):
3412 warning = newname + ' is not allowed for a project name.'
emayecsa71088b2021-08-14 18:02:58 -04003413 elif newname == "":
3414 warning = 'Please enter a project name.'
emayecs5966a532021-07-29 10:07:02 -04003415 elif badrex1.match(newname):
3416 warning = 'project name may not start with "."'
3417 elif badrex2.match(newname):
3418 warning = 'project name contains illegal characters or whitespace.'
3419 elif os.path.exists(newproject):
3420 warning = newname + ' is already a project name.'
3421 else:
3422 break
3423
3424 oldpath = value['values'][0]
3425 oldname = os.path.split(oldpath)[1]
3426 patterns = [oldname + '.log']
3427 if not elprefs:
3428 patterns.append('.java')
3429 if not spprefs:
3430 patterns.append('ngspice')
3431 patterns.append('pv')
3432
3433 print("New project name will be " + newname)
3434 try:
emayecsa71088b2021-08-14 18:02:58 -04003435 if os.path.islink(oldpath):
3436 os.symlink(oldpath, newproject)
3437 else:
3438 shutil.copytree(oldpath, newproject, symlinks = True,
3439 ignore = shutil.ignore_patterns(*patterns))
emayecs5966a532021-07-29 10:07:02 -04003440 except IOError as e:
3441 print('Error copying files: ' + str(e))
3442 return
3443
emayecsb2487ae2021-08-05 10:30:13 -04003444 # NOTE: Behavior is for project files to depend on "project_name". Using
emayecs5966a532021-07-29 10:07:02 -04003445 # the project filename as a project name is a fallback behavior. If
emayecsb2487ae2021-08-05 10:30:13 -04003446 # there is a info.yaml file, and it defines a project_name entry, then
emayecs5966a532021-07-29 10:07:02 -04003447 # there is no need to make changes within the project. If there is
emayecsb2487ae2021-08-05 10:30:13 -04003448 # no info.yaml file, then create one and set the project_name entry to
emayecs5966a532021-07-29 10:07:02 -04003449 # the old project name, which avoids the need to make changes within
3450 # the project.
3451
3452 else:
emayecsb2487ae2021-08-05 10:30:13 -04003453 # Check info.yaml
3454 yamlname = newproject + '/info.yaml'
emayecs5966a532021-07-29 10:07:02 -04003455
3456 found = False
emayecsb2487ae2021-08-05 10:30:13 -04003457 if os.path.isfile(yamlname):
emayecs12e85282021-08-11 09:37:00 -04003458 # Pull the project_name into local store (may want to do this with the
emayecs5966a532021-07-29 10:07:02 -04003459 # datasheet as well)
emayecsb2487ae2021-08-05 10:30:13 -04003460 with open(yamlname, 'r') as f:
3461 datatop = yaml.safe_load(f)
3462 if 'project_name' in datatop['project']:
emayecs5966a532021-07-29 10:07:02 -04003463 found = True
3464
3465 if not found:
emayecsb2487ae2021-08-05 10:30:13 -04003466 pdkdir = self.get_pdk_dir(newproject, path=True)
3467 yData = self.create_yaml(oldname, pdkdir)
3468 with open(newproject + '/info.yaml', 'w') as ofile:
3469 print('---',file=ofile)
3470 yaml.dump(yData, ofile)
emayecs5966a532021-07-29 10:07:02 -04003471
3472 # If ngspice and electric prefs were not copied from the source
3473 # to the target, as recommended, then copy these from the
3474 # skeleton repository as is done when creating a new project.
3475
3476 if not spprefs:
3477 try:
3478 os.makedirs(newproject + '/ngspice')
3479 os.makedirs(newproject + '/ngspice/run')
3480 os.makedirs(newproject + '/ngspice/run/.allwaves')
3481 except FileExistsError:
3482 pass
emayecsa71088b2021-08-14 18:02:58 -04003483 '''
emayecs5966a532021-07-29 10:07:02 -04003484 if not elprefs:
3485 # Copy preferences
3486 deskel = '/ef/efabless/deskel'
3487 try:
3488 shutil.copytree(deskel + '/dotjava', newproject + '/elec/.java', symlinks = True)
3489 except IOError as e:
3490 print('Error copying files: ' + e)
emayecsa71088b2021-08-14 18:02:58 -04003491 '''
emayecs5966a532021-07-29 10:07:02 -04003492
emayecsca79e462021-08-19 16:18:50 -04003493#----------------------------------------------------------------------
3494 # Allow the user to choose the flow of the project
emayecs5966a532021-07-29 10:07:02 -04003495 #----------------------------------------------------------------------
emayecsca79e462021-08-19 16:18:50 -04003496
3497 def startflow(self, value):
3498 projectpath = value['values'][0]
3499 flow = ''
3500 warning = 'Select a flow for '+value['text']
3501 is_subproject = False
3502 try:
3503 with open(os.path.expanduser(currdesign), 'r') as f:
3504 pdirCur = f.read().rstrip()
3505 if ('subcells' in pdirCur):
3506 # subproject is selected
3507 is_subproject = True
3508 except:
3509 pass
3510 if not os.path.exists(projectpath + '/info.yaml'):
3511 project_pdkdir = self.get_pdk_dir(projectpath, path=True)
3512 data = self.create_yaml(os.path.split(projectpath)[1], project_pdkdir)
3513 with open(projectpath + '/info.yaml', 'w') as ofile:
3514 print('---',file=ofile)
3515 yaml.dump(data, ofile)
3516
3517 # Read yaml file for the selected flow
3518 with open(projectpath + '/info.yaml','r') as f:
3519 data = yaml.safe_load(f)
3520 project = data['project']
3521 if 'flow' in project.keys() and project['flow']=='none' or 'flow' not in project.keys():
3522 while True:
3523 try:
3524 flow = SelectFlowDialog(self, warning, seed='', is_subproject = is_subproject).result
3525 except TypeError:
3526 # TypeError occurs when "Cancel" is pressed, just handle exception.
3527 return None
3528 if not flow:
3529 return None # Canceled, no action.
3530 break
3531 project['flow']=flow
3532 data['project']=project
3533 with open(projectpath + '/info.yaml', 'w') as ofile:
3534 print('---',file=ofile)
3535 yaml.dump(data, ofile)
3536 else:
3537 flow = project['flow']
3538
3539 print("Starting "+flow+" flow...")
3540 if flow.lower() == 'digital':
3541 self.synthesize()
3542
3543 #----------------------------------------------------------------------
emayecs5966a532021-07-29 10:07:02 -04003544 # Change a project IP to a different name.
3545 #----------------------------------------------------------------------
3546
3547 def renameproject(self, value):
3548 if not value['values']:
3549 print('No project selected.')
3550 return
3551
3552 # Require new project name and confirmation
3553 badrex1 = re.compile("^\.")
3554 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3555 projname = value['text']
3556
emayecsb2487ae2021-08-05 10:30:13 -04003557 # Find the IP name for project projname. If it has a YAML file, then
emayecs5966a532021-07-29 10:07:02 -04003558 # read it and pull the ip-name record. If not, the fallback position
3559 # is to assume that the project filename is the project name.
3560
emayecsb2487ae2021-08-05 10:30:13 -04003561 # Check info.yaml
emayecs5966a532021-07-29 10:07:02 -04003562 projectpath = self.projectdir + '/' + projname
emayecsb2487ae2021-08-05 10:30:13 -04003563 yamlname = projectpath + '/info.yaml'
emayecs5966a532021-07-29 10:07:02 -04003564
3565 oldname = projname
emayecsb2487ae2021-08-05 10:30:13 -04003566 if os.path.isfile(yamlname):
emayecs5966a532021-07-29 10:07:02 -04003567 # Pull the ipname into local store (may want to do this with the
3568 # datasheet as well)
emayecsb2487ae2021-08-05 10:30:13 -04003569 with open(yamlname, 'r') as f:
3570 datatop = yaml.safe_load(f)
3571 project_data = datatop['project']
3572 if 'project_name' in project_data:
3573 oldname = project_data['project_name']
emayecs5966a532021-07-29 10:07:02 -04003574
3575 warning = 'Rename IP "' + oldname + '" for project ' + projname + ':'
3576 print(warning)
3577 newname = projname
3578 while True:
3579 try:
3580 newname = ProjectNameDialog(self, warning, seed=oldname + '_1').result
3581 except TypeError:
3582 # TypeError occurs when "Cancel" is pressed, just handle exception.
3583 return None
3584 if not newname:
3585 return None # Canceled, no action.
3586
3587 if self.blacklisted(newname):
3588 warning = newname + ' is not allowed for an IP name.'
3589 elif badrex1.match(newname):
3590 warning = 'IP name may not start with "."'
3591 elif badrex2.match(newname):
3592 warning = 'IP name contains illegal characters or whitespace.'
3593 else:
3594 break
3595
3596 # Update everything, including schematic, symbol, layout, JSON file, etc.
3597 print('New project IP name will be ' + newname + '.')
3598 rename_project_all(projectpath, newname)
3599
3600 # class vars: one-time compile of regulare expressions for life of the process
3601 projNameBadrex1 = re.compile("^[-.]")
3602 projNameBadrex2 = re.compile(".*[][{}()!/ \t\n\\\><#$\*\?\"'|`~]")
3603 importProjNameBadrex1 = re.compile(".*[.]bak$")
3604
3605 # centralize legal projectName check.
3606 # TODO: Several code sections are not yet converted to use this.
3607 # TODO: Extend to explain to the user the reason why.
3608 def validProjectName(self, name):
3609 return not (self.blacklisted(name) or
3610 self.projNameBadrex1.match(name) or
3611 self.projNameBadrex2.match(name))
3612
emayecs12e85282021-08-11 09:37:00 -04003613#----------------------------------------------------------------------
emayecs582bc382021-08-13 12:32:12 -04003614 # Import a project or subproject to the project manager
emayecs5966a532021-07-29 10:07:02 -04003615 #----------------------------------------------------------------------
emayecs12e85282021-08-11 09:37:00 -04003616
3617 def importproject(self, value):
3618 warning = "Import project:"
3619 badrex1 = re.compile("^\.")
3620 badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
3621 print(warning)
3622
emayecs582bc382021-08-13 12:32:12 -04003623 # 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 -04003624 parent_pdk = ''
emayecs582bc382021-08-13 12:32:12 -04003625 parent_path = ''
emayecs12e85282021-08-11 09:37:00 -04003626 try:
3627 with open(os.path.expanduser(currdesign), 'r') as f:
3628 pdirCur = f.read().rstrip()
3629 if ('subcells' in pdirCur):
3630 # subproject is selected
3631 parent_path = os.path.split(os.path.split(pdirCur)[0])[0]
3632 parent_pdkdir = self.get_pdk_dir(parent_path, path=True)
3633 (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( parent_pdkdir )
3634 parent_pdk = foundry + '/' + node
3635 warning = 'Import a subproject to '+ parent_path + ':'
3636 elif (pdirCur[0] == '.'):
3637 # the project's 'subproject' of itself is selected
3638 parent_path = pdirCur[1:]
3639 parent_pdkdir = self.get_pdk_dir(parent_path, path=True)
3640 (foundry, foundry_name, node, desc, status) = self.pdkdir2fnd( parent_pdkdir )
3641 parent_pdk = foundry + '/' + node
3642 warning = 'Import a subproject to '+ parent_path + ':'
3643
3644 except:
3645 pass
3646
3647 while True:
3648 try:
emayecs582bc382021-08-13 12:32:12 -04003649 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 -04003650 except TypeError:
3651 # TypeError occurs when "Cancel" is pressed, just handle exception.
3652 return None
3653 if not newname:
3654 return None # Canceled, no action.
3655
3656 if parent_pdk == '':
3657 newproject = self.projectdir + '/' + newname
3658 else:
3659 newproject = parent_path + '/subcells/' + newname
emayecs582bc382021-08-13 12:32:12 -04003660 break
3661
3662 def make_techdirs(projectpath, project_pdkdir):
3663 # Recursively create techdirs in project and subproject folders
emayecs12e85282021-08-11 09:37:00 -04003664 if not (os.path.exists(projectpath + '/.config') or os.path.exists(projectpath + '/.ef-config')):
3665 os.makedirs(projectpath + '/.config')
emayecs582bc382021-08-13 12:32:12 -04003666 if not os.path.exists(projectpath + self.config_path(projectpath) + '/techdir'):
3667 os.symlink(project_pdkdir, projectpath + self.config_path(projectpath) + '/techdir')
3668 if os.path.isdir(projectpath + '/subcells'):
3669 for subproject in os.listdir(projectpath + '/subcells'):
3670 subproject_path = projectpath + '/subcells/' + subproject
3671 make_techdirs(subproject_path, project_pdkdir)
3672
3673 make_techdirs(projectpath, project_pdkdir)
emayecs12e85282021-08-11 09:37:00 -04003674
emayecs582bc382021-08-13 12:32:12 -04003675 # Make symbolic link/copy projects
3676 if parent_path=='':
3677 # Create a regular project
3678 if importoption == "link":
3679 os.symlink(projectpath, self.projectdir + '/' + newname)
3680 else:
emayecs582bc382021-08-13 12:32:12 -04003681 shutil.copytree(projectpath, self.projectdir + '/' + newname, symlinks = True)
3682 if not os.path.exists(projectpath + '/info.yaml'):
3683 yData = self.create_yaml(newname, project_pdkdir)
3684 with open(projectpath + '/info.yaml', 'w') as ofile:
3685 print('---',file=ofile)
3686 yaml.dump(yData, ofile)
emayecs12e85282021-08-11 09:37:00 -04003687 else:
emayecs582bc382021-08-13 12:32:12 -04003688 #Create a subproject
emayecs12e85282021-08-11 09:37:00 -04003689 if not os.path.exists(parent_path + '/subcells'):
3690 os.makedirs(parent_path + '/subcells')
emayecs582bc382021-08-13 12:32:12 -04003691 if importoption == "copy":
emayecs582bc382021-08-13 12:32:12 -04003692 shutil.copytree(projectpath, parent_path + '/subcells/' + newname, symlinks = True)
3693 if parent_pdkdir != project_pdkdir:
3694 self.clean(parent_path + '/subcells/' + newname)
3695 else:
3696 os.symlink(projectpath, parent_path + '/subcells/' + newname)
3697 if not os.path.exists(parent_path + '/subcells/' + newname + '/info.yaml'):
3698 yData = self.create_yaml(newname, project_pdkdir)
3699 with open(parent_path + '/subcells/' + newname + '/info.yaml', 'w') as ofile:
3700 print('---',file=ofile)
3701 yaml.dump(yData, ofile)
3702 self.update_project_views()
emayecs12e85282021-08-11 09:37:00 -04003703 #----------------------------------------------------------------------
emayecs5966a532021-07-29 10:07:02 -04003704 # "Import As" a dir in import/ as a project. based on renameproject().
3705 # addWarn is used to augment confirm-dialogue if redirected here via erroneous ImportInto
3706 #----------------------------------------------------------------------
3707
3708 def import2project(self, importfile, addWarn=None):
3709 name = os.path.split(importfile)[1]
3710 projpath = self.projectdir + '/' + name
3711
3712 bakname = name + '.bak'
3713 bakpath = self.projectdir + '/' + bakname
3714 warns = []
3715 if addWarn:
3716 warns += [ addWarn ]
3717
3718 # Require new project name and confirmation
3719 confirmPrompt = None # use default: I am sure I want to do this.
3720 if os.path.isdir(projpath):
3721 if warns:
3722 warns += [ '' ] # blank line between addWarn and below two Warnings:
3723 if os.path.isdir(bakpath):
3724 warns += [ 'Warning: Replacing EXISTING: ' + name + ' AND ' + bakname + '!' ]
3725 else:
3726 warns += [ 'Warning: Replacing EXISTING: ' + name + '!' ]
3727 warns += [ 'Warning: Check for & exit any Electric,magic,qflow... for above project(s)!\n' ]
3728 confirmPrompt = 'I checked & exited apps and am sure I want to do this.'
3729
3730 warns += [ 'Confirm import-as new project: ' + name + '?' ]
3731 warning = '\n'.join(warns)
3732 confirm = ProtectedConfirmDialog(self, warning, confirmPrompt=confirmPrompt).result
3733 if not confirm == 'okay':
3734 return
3735
3736 print('New project name will be ' + name + '.')
3737 try:
3738 if os.path.isdir(projpath):
3739 if os.path.isdir(bakpath):
3740 print('Deleting old project: ' + bakpath);
3741 shutil.rmtree(bakpath)
3742 print('Moving old project ' + name + ' to ' + bakname)
3743 os.rename( projpath, bakpath)
3744 print("Importing as new project " + name)
3745 os.rename(importfile, projpath)
3746 return True
3747 except IOError as e:
3748 print("Error importing-as project: " + str(e))
3749 return None
3750
3751 #----------------------------------------------------------------------
3752 # Helper subroutine:
3753 # Check if a project is a valid project. Return the name of the
3754 # datasheet if the project has a valid one in the project top level
3755 # path.
3756 #----------------------------------------------------------------------
3757
3758 def get_datasheet_name(self, dpath):
3759 if not os.path.isdir(dpath):
3760 print('Error: Project is not a folder!')
3761 return
3762 # Check for valid datasheet name in the following order:
3763 # (1) project.json (Legacy)
3764 # (2) <name of directory>.json (Legacy)
3765 # (3) not "datasheet.json" or "datasheet_anno.json"
3766 # (4) "datasheet.json"
3767 # (5) "datasheet_anno.json"
3768
3769 dsname = os.path.split(dpath)[1]
3770 if os.path.isfile(dpath + '/project.json'):
3771 datasheet = dpath + '/project.json'
3772 elif os.path.isfile(dpath + '/' + dsname + '.json'):
3773 datasheet = dpath + '/' + dsname + '.json'
3774 else:
3775 has_generic = False
3776 has_generic_anno = False
3777 filelist = os.listdir(dpath)
3778 for file in filelist[:]:
3779 if os.path.splitext(file)[1] != '.json':
3780 filelist.remove(file)
3781 if 'datasheet.json' in filelist:
3782 has_generic = True
3783 filelist.remove('datasheet.json')
3784 if 'datasheet_anno.json' in filelist:
3785 has_generic_anno = True
3786 filelist.remove('datasheet_anno.json')
3787 if len(filelist) == 1:
3788 print('Trying ' + dpath + '/' + filelist[0])
3789 datasheet = dpath + '/' + filelist[0]
3790 elif has_generic:
3791 datasheet + dpath + '/datasheet.json'
3792 elif has_generic_anno:
3793 datasheet + dpath + '/datasheet_anno.json'
3794 else:
3795 if len(filelist) > 1:
3796 print('Error: Path ' + dpath + ' has ' + str(len(filelist)) +
3797 ' valid datasheets.')
3798 else:
3799 print('Error: Path ' + dpath + ' has no valid datasheets.')
3800 return None
3801
3802 if not os.path.isfile(datasheet):
3803 print('Error: File ' + datasheet + ' not found.')
3804 return None
3805 else:
3806 return datasheet
3807
3808 #----------------------------------------------------------------------
3809 # Run the LVS manager
3810 #----------------------------------------------------------------------
3811
3812 def run_lvs(self):
3813 value = self.projectselect.selected()
3814 if value:
3815 design = value['values'][0]
3816 # designname = value['text']
3817 designname = self.project_name
3818 print('Run LVS on design ' + designname + ' (' + design + ')')
3819 # use Popen, not run, so that application does not wait for it to exit.
emayecs5656b2b2021-08-04 12:44:13 -04003820 subprocess.Popen(['netgen','-gui',design, designname])
emayecs5966a532021-07-29 10:07:02 -04003821 else:
3822 print("You must first select a project.", file=sys.stderr)
3823
3824 #----------------------------------------------------------------------
3825 # Run the local characterization checker
3826 #----------------------------------------------------------------------
3827
3828 def characterize(self):
3829 value = self.projectselect.selected()
3830 if value:
3831 design = value['values'][0]
3832 # designname = value['text']
3833 designname = self.project_name
3834 datasheet = self.get_datasheet_name(design)
3835 print('Characterize design ' + designname + ' (' + datasheet + ' )')
3836 if datasheet:
3837 # use Popen, not run, so that application does not wait for it to exit.
3838 dsheetroot = os.path.splitext(datasheet)[0]
emayecsf31fb7a2021-08-04 16:27:55 -04003839 subprocess.Popen([config.apps_path + '/cace.py',
emayecs5966a532021-07-29 10:07:02 -04003840 datasheet])
3841 else:
3842 print("You must first select a project.", file=sys.stderr)
3843
3844 #----------------------------------------------------------------------
3845 # Run the local synthesis tool (qflow)
3846 #----------------------------------------------------------------------
3847
3848 def synthesize(self):
3849 value = self.projectselect.selected()
3850 if value:
emayecsca79e462021-08-19 16:18:50 -04003851 design = value['values'][0] # project path
3852 pdkdir = self.get_pdk_dir(design, path = True)
emayecs5eda03a2021-08-23 12:33:22 -04003853 qflowdir = pdkdir + '/libs.tech/qflow'
emayecs5966a532021-07-29 10:07:02 -04003854 # designname = value['text']
3855 designname = self.project_name
3856 development = self.prefs['devstdcells']
3857 if not designname:
3858 # A project without a datasheet has no designname (which comes from
3859 # the 'ip-name' record in the datasheet JSON) but can still be
3860 # synthesized.
3861 designname = design
3862
3863 # Normally there is one digital design in a project. However, full-chip
3864 # designs (in particular) may have multiple sub-projects that are
3865 # independently synthesized digital blocks. Find all subdirectories of
3866 # the top level or subdirectories of qflow that contain a 'qflow_vars.sh'
3867 # file. If there is more than one, then present a list. If there is
3868 # only one but it is not in 'qflow/', then be sure to pass the actual
3869 # directory name to the qflow manager.
emayecs5966a532021-07-29 10:07:02 -04003870 qvlist = glob.glob(design + '/*/qflow_vars.sh')
3871 qvlist.extend(glob.glob(design + '/qflow/*/qflow_vars.sh'))
3872 if len(qvlist) > 1 or (len(qvlist) == 1 and not os.path.exists(design + '/qflow/qflow_vars.sh')):
3873 # Generate selection menu
3874 if len(qvlist) > 1:
3875 clist = list(os.path.split(item)[0] for item in qvlist)
3876 ppath = ExistingProjectDialog(self, clist, warning="Enter name of qflow project to open:").result
3877 if not ppath:
3878 return 0 # Canceled in dialog, no action.
3879 else:
3880 ppath = os.path.split(qvlist[0])[0]
3881
3882 # pname is everything in ppath after matching design:
3883 pname = ppath.replace(design + '/', '')
3884
3885 print('Synthesize design in qflow project directory ' + pname)
emayecsca79e462021-08-19 16:18:50 -04003886 print('Loading digital flow manager...')
3887 #TODO: replace hard-coded path with function that gets the qflow manager path
emayecs5966a532021-07-29 10:07:02 -04003888 if development:
emayecsca79e462021-08-19 16:18:50 -04003889 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
emayecs3f17d542021-08-20 17:48:28 -04003890 qflowdir, design, '-development', '-subproject=' + pname])
emayecs5966a532021-07-29 10:07:02 -04003891 else:
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, '-subproject=' + pname])
emayecs5966a532021-07-29 10:07:02 -04003894 else:
3895 print('Synthesize design ' + designname + ' (' + design + ')')
emayecsca79e462021-08-19 16:18:50 -04003896 print('Loading digital flow manager...')
emayecs5966a532021-07-29 10:07:02 -04003897 # use Popen, not run, so that application does not wait for it to exit.
3898 if development:
emayecsca79e462021-08-19 16:18:50 -04003899 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
emayecs3f17d542021-08-20 17:48:28 -04003900 qflowdir, design, designname, '-development'])
emayecs5966a532021-07-29 10:07:02 -04003901 else:
emayecsca79e462021-08-19 16:18:50 -04003902 subprocess.Popen(['/usr/local/share/qflow/scripts/qflow_manager.py',
emayecs3f17d542021-08-20 17:48:28 -04003903 qflowdir, design, designname])
emayecs5966a532021-07-29 10:07:02 -04003904 else:
3905 print("You must first select a project.", file=sys.stderr)
3906
3907 #----------------------------------------------------------------------
3908 # Switch between showing and hiding the import list (default hidden)
3909 #----------------------------------------------------------------------
3910
3911 def import_toggle(self):
3912 import_state = self.toppane.import_frame.import_header3.cget('text')
3913 if import_state == '+':
3914 self.importselect.grid(row = 11, sticky = 'news')
3915 self.toppane.import_frame.import_header3.config(text='-')
3916 else:
3917 self.importselect.grid_forget()
3918 self.toppane.import_frame.import_header3.config(text='+')
3919
3920 def import_open(self):
3921 self.importselect.grid(row = 11, sticky = 'news')
3922 self.toppane.import_frame.import_header3.config(text='-')
3923
3924 #----------------------------------------------------------------------
3925 # Switch between showing and hiding the IP library list (default hidden)
3926 #----------------------------------------------------------------------
3927
3928 def library_toggle(self):
3929 library_state = self.toppane.library_frame.library_header3.cget('text')
3930 if library_state == '+':
3931 self.ipselect.grid(row = 8, sticky = 'news')
3932 self.toppane.library_frame.library_header3.config(text='-')
3933 else:
3934 self.ipselect.grid_forget()
3935 self.toppane.library_frame.library_header3.config(text='+')
3936
3937 def library_open(self):
3938 self.ipselect.grid(row = 8, sticky = 'news')
3939 self.toppane.library_frame.library_header3.config(text='-')
3940
3941 #----------------------------------------------------------------------
3942 # Run padframe-calc (today internally invokes libreoffice, we only need cwd set to design project)
3943 #----------------------------------------------------------------------
3944 def padframe_calc(self):
3945 value = self.projectselect.selected()
3946 if value:
3947 designname = self.project_name
3948 self.padframe_calc_work(newname=designname)
3949 else:
3950 print("You must first select a project.", file=sys.stderr)
3951
3952 #------------------------------------------------------------------------
3953 # Run padframe-calc (today internally invokes libreoffice, we set cwd to design project)
3954 # Modelled somewhat after 'def importvgl':
3955 # Prompt for an existing electric lib.
3956 # Prompt for a target cellname (for both mag and electric icon).
3957 # (The AS vs INTO behavior is incomplete as yet. Used so far with current-project as newname arg).
3958 # newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
3959 # Either newname is given: we PROMPT to pick an existing elecLib;
3960 # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
3961 #------------------------------------------------------------------------
3962 def padframe_calc_work(self, newname=None):
3963 elecLib = newname
3964 isnew = not newname
3965 if isnew:
3966 # Use create project code first to generate a valid project space.
3967 newname = self.createproject(None)
3968 if not newname: return 0 # Canceled in dialog, no action.
3969 # print("padframe-calc in new project " + newname + ".")
3970 elecLib = newname
3971
3972 # For life of this projectManager process, store/recall last PadFrame Settings per project
3973 global project2pfd
3974 try:
3975 project2pfd
3976 except:
3977 project2pfd = {}
3978 if newname not in project2pfd:
3979 project2pfd[newname] = {"libEntry": None, "cellName": None}
3980
3981 ppath = self.projectdir + '/' + newname
3982 choices = self.get_elecLib_list(newname)
3983 if not choices:
3984 print( "Aborted: No existing electric libraries found to write symbol into.")
3985 return 0
3986
3987 elecLib = newname + '/elec/' + elecLib + '.delib'
3988 elecLib = project2pfd[newname]["libEntry"] or elecLib
3989 cellname = project2pfd[newname]["cellName"] or "padframe"
3990 libAndCell = ExistingElecLibCellDialog(self, None, title="PadFrame Settings", plist=choices, descPost="of icon&layout", seedLibNm=elecLib, seedCellNm=cellname).result
3991 if not libAndCell:
3992 return 0 # Canceled in dialog, no action.
3993
3994 (elecLib, cellname) = libAndCell
3995 if not cellname:
3996 return 0 # empty cellname, no action.
3997
3998 project2pfd[newname]["libEntry"] = elecLib
3999 project2pfd[newname]["cellName"] = cellname
4000
4001 # Isolate just electric lib name without extension. ../a/b.delib -> b
4002 elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
4003 print("padframe-calc in project: %s, elecLib: %s, cellName: %s" % (newname, elecLib, cellname))
4004
4005 export = dict(os.environ)
4006 export['EF_DESIGNDIR'] = ppath
4007 subprocess.Popen(['/ef/apps/bin/padframe-calc', elecLib, cellname], cwd = ppath, env = export)
4008
4009 # not yet any useful return value or reporting of results here in projectManager...
4010 return 1
4011
4012 #----------------------------------------------------------------------
4013 # Run the schematic editor (tool as given by user preference)
4014 #----------------------------------------------------------------------
4015
4016 def edit_schematic(self):
4017 value = self.projectselect.selected()
4018 if value:
4019 design = value['values'][0]
4020
4021 pdktechdir = design + self.config_path(design)+'/techdir/libs.tech'
4022
4023 applist = self.list_valid_schematic_editors(pdktechdir)
4024
4025 if len(applist)==0:
4026 print("Unable to find a valid schematic editor.")
4027 return
4028
4029 # If the preferred app is in the list, then use it.
4030
4031 if self.prefs['schemeditor'] in applist:
4032 appused = self.prefs['schemeditor']
4033 else:
4034 appused = applist[0]
4035
4036 if appused == 'xcircuit':
4037 return self.edit_schematic_with_xcircuit()
4038 elif appused == 'xschem':
4039 return self.edit_schematic_with_xschem()
4040 elif appused == 'electric':
4041 return self.edit_schematic_with_electric()
4042 else:
4043 print("Unknown/unsupported schematic editor " + appused + ".", file=sys.stderr)
4044
4045 else:
4046 print("You must first select a project.", file=sys.stderr)
4047
4048 #----------------------------------------------------------------------
4049 # Run the schematic editor (electric)
4050 #----------------------------------------------------------------------
4051
4052 def edit_schematic_with_electric(self):
4053 value = self.projectselect.selected()
4054 if value:
4055 design = value['values'][0]
4056 # designname = value['text']
4057 # self.project_name set by setcurrent. This is the true project
4058 # name, as opposed to the directory name.
4059 designname = self.project_name
4060 print('Edit schematic ' + designname + ' (' + design + ' )')
4061 # Collect libs on command-line; electric opens these in Explorer
4062 libs = []
4063 ellibrex = re.compile(r'^(tech_.*|ef_examples)\.[dj]elib$', re.IGNORECASE)
4064
4065 self.reinitElec(design)
4066
4067 # /elec and /.java are prerequisites for running electric
4068 if not os.path.exists(design + '/elec'):
4069 print("No path to electric design folder.")
4070 return
4071
4072 if not os.path.exists(design + '/elec/.java'):
4073 print("No path to electric .java folder.")
4074 return
4075
4076 # Fix the LIBDIRS file if needed
4077 #fix_libdirs(design, create = True)
4078
4079 # Check for legacy directory (missing .ef-config and/or .ef-config/techdir);
4080 # Handle as necessary.
4081
4082 # don't sometimes yield pdkdir as some subdir of techdir
4083 pdkdir = design + self.config_path(design) + '/techdir/'
4084 if not os.path.exists(pdkdir):
4085 export = dict(os.environ)
4086 export['EF_DESIGNDIR'] = design
4087 '''
4088 p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
4089 stdout = subprocess.PIPE, env = export)
4090 config_out = p.stdout.splitlines()
4091 for line in config_out:
4092 setline = line.decode('utf-8').split('=')
4093 if setline[0] == 'EF_TECHDIR':
4094 pdkdir = re.sub("[';]", "", setline[1])
4095 '''
4096
4097 for subpath in ('libs.tech/elec/', 'libs.ref/elec/'):
4098 pdkelec = os.path.join(pdkdir, subpath)
4099 if os.path.exists(pdkelec) and os.path.isdir(pdkelec):
4100 # don't use os.walk(), it is recursive, wastes time
4101 for entry in os.scandir(pdkelec):
4102 if ellibrex.match(entry.name):
4103 libs.append(entry.path)
4104
4105 # Locate most useful project-local elec-lib to open on electric cmd-line.
4106 designroot = os.path.split(design)[1]
4107 finalInDesDirLibAdded = False
4108 if os.path.exists(design + '/elec/' + designname + '.jelib'):
4109 libs.append(design + '/elec/' + designname + '.jelib')
4110 finalInDesDirLibAdded = True
4111 elif os.path.isdir(design + '/elec/' + designname + '.delib'):
4112 libs.append(design + '/elec/' + designname + '.delib')
4113 finalInDesDirLibAdded = True
4114 else:
4115 # Alternative path is the project name + .delib
4116 if os.path.isdir(design + '/elec/' + designroot + '.delib'):
4117 libs.append(design + '/elec/' + designroot + '.delib')
4118 finalInDesDirLibAdded = True
4119
4120 # Finally, check for the one absolute requirement for a project,
4121 # which is that there must be a symbol designname + .ic in the
4122 # last directory. If not, then do a search for it.
4123 if not finalInDesDirLibAdded or not os.path.isfile(libs[-1] + '/' + designname + '.ic'):
4124 delibdirs = os.listdir(design + '/elec')
4125 for delibdir in delibdirs:
4126 if os.path.splitext(delibdir)[1] == '.delib':
4127 iconfiles = os.listdir(design + '/elec/' + delibdir)
4128 for iconfile in iconfiles:
4129 if iconfile == designname + '.ic':
4130 libs.append(design + '/elec/' + delibdir)
4131 finalInDesDirLibAdded = True
4132 break
4133
4134 # Above project-local lib-adds are all conditional on finding some lib
4135 # with an expected name or content: all of which may fail.
4136 # Force last item ALWAYS to be 'a path' in the project's elec/ dir.
4137 # Usually it's a real library (found above). (If lib does not exist the messages
4138 # window does get an error message). But the purpose is for the universal side-effect:
4139 # To EVERY TIME reseed the File/OpenLibrary dialogue WorkDir to start in
4140 # project's elec/ dir; avoid it starting somewhere in the PDK, which
4141 # is what will happen if last actual cmd-line arg is a lib in the PDK, and
4142 # about which users have complained. (Optimal fix needs electric enhancement).
4143 if not finalInDesDirLibAdded:
4144 libs.append(design + '/elec/' + designroot + '.delib')
4145
4146 # Pull last item from libs and make it a command-line argument.
4147 # All other libraries become part of the EOPENARGS environment variable,
4148 # and electric is called with the elecOpen.bsh script.
4149 indirectlibs = libs[:-1]
4150 export = dict(os.environ)
4151 arguments = []
4152 if indirectlibs:
4153 export['EOPENARGS'] = ' '.join(indirectlibs)
4154 arguments.append('-s')
4155 arguments.append('/ef/efabless/lib/elec/elecOpen.bsh')
4156
4157 try:
4158 arguments.append(libs[-1])
4159 except IndexError:
4160 print('Error: Electric project directories not set up correctly?')
4161 else:
4162 subprocess.Popen(['electric', *arguments], cwd = design + '/elec',
4163 env = export)
4164 else:
4165 print("You must first select a project.", file=sys.stderr)
4166
4167 #----------------------------------------------------------------------
4168 # Run the schematic editor (xcircuit)
4169 #----------------------------------------------------------------------
4170
4171 def edit_schematic_with_xcircuit(self):
4172 value = self.projectselect.selected()
4173 if value:
4174 design = value['values'][0]
4175 # designname = value['text']
4176 # self.project_name set by setcurrent. This is the true project
4177 # name, as opposed to the directory name.
4178 designname = self.project_name
4179 print('Edit schematic ' + designname + ' (' + design + ' )')
4180 xcircdirpath = design + '/xcirc'
4181 pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xcircuit'
4182
4183 # /xcirc directory is a prerequisite for running xcircuit. If it doesn't
4184 # exist, create it and seed it with .xcircuitrc from the tech directory
4185 if not os.path.exists(xcircdirpath):
4186 os.makedirs(xcircdirpath)
4187
4188 # Copy xcircuit startup file from tech directory
4189 hasxcircrcfile = os.path.exists(xcircdirpath + '/.xcircuitrc')
4190 if not hasxcircrcfile:
4191 if os.path.exists(pdkdir + '/xcircuitrc'):
4192 shutil.copy(pdkdir + '/xcircuitrc', xcircdirpath + '/.xcircuitrc')
4193
4194 # Command line argument is the project name
4195 arguments = [design + '/xcirc' + designname]
4196 subprocess.Popen(['xcircuit', *arguments])
4197 else:
4198 print("You must first select a project.", file=sys.stderr)
4199
4200 #----------------------------------------------------------------------
4201 # Run the schematic editor (xschem)
4202 #----------------------------------------------------------------------
4203
4204 def edit_schematic_with_xschem(self):
4205 value = self.projectselect.selected()
4206 if value:
4207 design = value['values'][0]
4208 # self.project_name set by setcurrent. This is the true project
4209 # name, as opposed to the directory name.
4210 designname = self.project_name
4211 print('Edit schematic ' + designname + ' (' + design + ' )')
4212 xschemdirpath = design + '/xschem'
4213
4214 pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xschem'
4215
4216
4217 # /xschem directory is a prerequisite for running xschem. If it doesn't
4218 # exist, create it and seed it with xschemrc from the tech directory
4219 if not os.path.exists(xschemdirpath):
4220 os.makedirs(xschemdirpath)
4221
4222 # Copy xschem startup file from tech directory
4223 hasxschemrcfile = os.path.exists(xschemdirpath + '/xschemrc')
4224 if not hasxschemrcfile:
4225 if os.path.exists(pdkdir + '/xschemrc'):
4226 shutil.copy(pdkdir + '/xschemrc', xschemdirpath + '/xschemrc')
4227
4228 # Command line argument is the project name. The "-r" option is recommended if there
4229 # is no stdin/stdout piping.
4230
4231 arguments = ['-r', design + '/xschem/' + designname]
4232 subprocess.Popen(['xschem', *arguments])
4233 else:
4234 print("You must first select a project.", file=sys.stderr)
4235
4236 #----------------------------------------------------------------------
4237 # Run the layout editor (magic or klayout)
4238 #----------------------------------------------------------------------
4239
4240 def edit_layout(self):
4241 value = self.projectselect.selected()
4242 if value:
4243 design = value['values'][0]
4244 pdktechdir = design + self.config_path(design) + '/techdir/libs.tech'
4245
4246 applist = self.list_valid_layout_editors(pdktechdir)
4247
4248 if len(applist)==0:
4249 print("Unable to find a valid layout editor.")
4250 return
4251
4252 # If the preferred app is in the list, then use it.
4253 if self.prefs['layouteditor'] in applist:
4254 appused = self.prefs['layouteditor']
4255 else:
4256 appused = applist[0]
4257
4258 if appused == 'magic':
4259 return self.edit_layout_with_magic()
4260 elif appused == 'klayout':
4261 return self.edit_layout_with_klayout()
4262 elif appused == 'electric':
4263 return self.edit_layout_with_electric()
4264 else:
4265 print("Unknown/unsupported layout editor " + appused + ".", file=sys.stderr)
4266
4267 else:
4268 print("You must first select a project.", file=sys.stderr)
4269
4270 #----------------------------------------------------------------------
4271 # Run the magic layout editor
4272 #----------------------------------------------------------------------
4273
4274 def edit_layout_with_magic(self):
4275 value = self.projectselect.selected()
4276 if value:
4277 design = value['values'][0]
4278 # designname = value['text']
4279 designname = self.project_name
4280
4281 pdkdir = ''
4282 pdkname = ''
4283
4284 if os.path.exists(design + '/.ef-config/techdir/libs.tech'):
4285 pdkdir = design + '/.ef-config/techdir/libs.tech/magic/current'
4286 pdkname = os.path.split(os.path.realpath(design + '/.ef-config/techdir'))[1]
4287 elif os.path.exists(design + '/.config/techdir/libs.tech'):
4288 pdkdir = design + '/.config/techdir/libs.tech/magic'
4289 pdkname = os.path.split(os.path.realpath(design + '/.config/techdir'))[1]
4290
4291
4292 # Check if the project has a /mag directory. Create it and
4293 # put the correct .magicrc file in it, if it doesn't.
4294 magdirpath = design + '/mag'
4295 hasmagdir = os.path.exists(magdirpath)
4296 if not hasmagdir:
4297 os.makedirs(magdirpath)
4298
4299 hasmagrcfile = os.path.exists(magdirpath + '/.magicrc')
4300 if not hasmagrcfile:
4301 shutil.copy(pdkdir + '/' + pdkname + '.magicrc', magdirpath + '/.magicrc')
4302
4303 # Check if the .mag file exists for the project. If not,
4304 # generate a dialog.
4305 magpath = design + '/mag/' + designname + '.mag'
4306 netpath = design + '/spi/' + designname + '.spi'
4307 # print("magpath is " + magpath)
4308 hasmag = os.path.exists(magpath)
4309 hasnet = os.path.exists(netpath)
4310 if hasmag:
4311 if hasnet:
4312 statbuf1 = os.stat(magpath)
4313 statbuf2 = os.stat(netpath)
4314 # No specific action for out-of-date layout. To be done:
4315 # Check contents and determine if additional devices need to
4316 # be added to the layout. This may be more trouble than it's
4317 # worth.
4318 #
4319 # if statbuf2.st_mtime > statbuf1.st_mtime:
4320 # hasmag = False
4321
4322 if not hasmag:
4323 # Does the project have any .mag files at all? If so, the project
4324 # layout may be under a name different than the project name. If
4325 # so, present the user with a selectable list of layout names,
4326 # with the option to start a new layout or import from schematic.
4327
4328 maglist = os.listdir(design + '/mag/')
4329 if len(maglist) > 1:
4330 # Generate selection menu
4331 warning = 'No layout matches IP name ' + designname + '.'
4332 maglist = list(item for item in maglist if os.path.splitext(item)[1] == '.mag')
4333 clist = list(os.path.splitext(item)[0] for item in maglist)
4334 ppath = EditLayoutDialog(self, clist, ppath=design,
4335 pname=designname, warning=warning,
4336 hasnet=hasnet).result
4337 if not ppath:
4338 return 0 # Canceled in dialog, no action.
4339 elif ppath != '(New layout)':
4340 hasmag = True
4341 designname = ppath
4342 elif len(maglist) == 1:
4343 # Only one magic file, no selection, just bring it up.
4344 designname = os.path.split(maglist[0])[1]
4345 hasmag = True
4346
4347 if not hasmag:
4348 populate = NewLayoutDialog(self, "No layout for project.").result
4349 if not populate:
4350 return 0 # Canceled, no action.
4351 elif populate():
4352 # Name of PDK deprecated. The .magicrc file in the /mag directory
4353 # will load the correct PDK and specify the proper library for the
4354 # low-level device namespace, which may not be the same as techdir.
4355 # NOTE: netlist_to_layout script will attempt to generate a
4356 # schematic netlist if one does not exist.
4357
4358 print('Running /ef/efabless/bin/netlist_to_layout.py ../spi/' + designname + '.spi')
4359 try:
4360 p = subprocess.run(['/ef/efabless/bin/netlist_to_layout.py',
4361 '../spi/' + designname + '.spi'],
4362 stdin = subprocess.PIPE, stdout = subprocess.PIPE,
4363 stderr = subprocess.PIPE, cwd = design + '/mag')
4364 if p.stderr:
4365 err_string = p.stderr.splitlines()[0].decode('utf-8')
4366 # Print error messages to console
4367 print(err_string)
4368
4369 except subprocess.CalledProcessError as e:
4370 print('Error running netlist_to_layout.py: ' + e.output.decode('utf-8'))
4371 else:
4372 if os.path.exists(design + '/mag/create_script.tcl'):
4373 with open(design + '/mag/create_script.tcl', 'r') as infile:
4374 magproc = subprocess.run(['/ef/apps/bin/magic',
4375 '-dnull', '-noconsole', '-rcfile ',
4376 pdkdir + '/' + pdkname + '.magicrc', designname],
4377 stdin = infile, stdout = subprocess.PIPE,
4378 stderr = subprocess.PIPE, cwd = design + '/mag')
4379 print("Populated layout cell")
4380 # os.remove(design + '/mag/create_script.tcl')
4381 else:
4382 print("No device generating script was created.", file=sys.stderr)
4383
4384 print('Edit layout ' + designname + ' (' + design + ' )')
4385
4386 magiccommand = ['magic']
4387 # Select the graphics package used by magic from the profile settings.
4388 if 'magic-graphics' in self.prefs:
4389 magiccommand.extend(['-d', self.prefs['magic-graphics']])
4390 # Check if .magicrc predates the latest and warn if so.
4391 statbuf1 = os.stat(design + '/mag/.magicrc')
4392 statbuf2 = os.stat(pdkdir + '/' + pdkname + '.magicrc')
4393 if statbuf2.st_mtime > statbuf1.st_mtime:
4394 print('NOTE: File .magicrc predates technology startup file. Using default instead.')
4395 magiccommand.extend(['-rcfile', pdkdir + '/' + pdkname + '.magicrc'])
4396 magiccommand.append(designname)
4397
4398 # Run magic and don't wait for it to finish
4399 subprocess.Popen(magiccommand, cwd = design + '/mag')
4400 else:
4401 print("You must first select a project.", file=sys.stderr)
4402
4403 #----------------------------------------------------------------------
4404 # Run the klayout layout editor
4405 #----------------------------------------------------------------------
4406
4407 def edit_layout_with_klayout(self):
4408 value = self.projectselect.selected()
4409 print("Klayout unsupported from project manager (work in progress); run manually", file=sys.stderr)
4410
4411 #----------------------------------------------------------------------
4412 # Run the electric layout editor
4413 #----------------------------------------------------------------------
4414
4415 def edit_layout_with_electric(self):
4416 value = self.projectselect.selected()
4417 print("Electric layout editing unsupported from project manager (work in progress); run manually", file=sys.stderr)
4418
4419 #----------------------------------------------------------------------
4420 # Upload design to the marketplace
4421 # NOTE: This is not being called by anything. Use version in the
4422 # characterization script, which can check for local results before
4423 # approving (or forcing) an upload.
4424 #----------------------------------------------------------------------
4425
4426 def upload(self):
4427 '''
4428 value = self.projectselect.selected()
4429 if value:
4430 design = value['values'][0]
4431 # designname = value['text']
4432 designname = self.project_name
4433 print('Upload design ' + designname + ' (' + design + ' )')
4434 subprocess.run(['/ef/apps/bin/withnet',
emayecsb2487ae2021-08-05 10:30:13 -04004435 config.apps_path + '/cace_design_upload.py',
emayecs5966a532021-07-29 10:07:02 -04004436 design, '-test'])
4437 '''
4438
4439 #--------------------------------------------------------------------------
4440 # Upload a datasheet to the marketplace (Administrative use only, for now)
4441 #--------------------------------------------------------------------------
4442
4443 # def make_challenge(self):
4444 # importp = self.cur_import
4445 # print("Make a Challenge from import " + importp + "!")
emayecsb2487ae2021-08-05 10:30:13 -04004446 # # subprocess.run([config.apps_path + '/cace_import_upload.py', importp, '-test'])
emayecs5966a532021-07-29 10:07:02 -04004447
emayecsc707a0a2021-08-06 17:13:27 -04004448 # Runs whenever a user selects a project
emayecs5966a532021-07-29 10:07:02 -04004449 def setcurrent(self, value):
4450 global currdesign
4451 treeview = value.widget
emayecsa71088b2021-08-14 18:02:58 -04004452 selection = treeview.item(treeview.selection()) # dict with text, values, tags, etc. as keys
emayecs5966a532021-07-29 10:07:02 -04004453 pname = selection['text']
emayecsa71088b2021-08-14 18:02:58 -04004454 pdir = treeview.selection()[0] # iid of the selected project
emayecs5966a532021-07-29 10:07:02 -04004455 #print("setcurrent returned value " + pname)
emayecsc707a0a2021-08-06 17:13:27 -04004456 metapath = os.path.expanduser(currdesign)
4457 if not os.path.exists(metapath):
4458 os.makedirs(os.path.split(metapath)[0], exist_ok=True)
4459 with open(metapath, 'w') as f:
emayecs55354d82021-08-08 15:57:32 -04004460 f.write(pdir + '\n')
emayecs5966a532021-07-29 10:07:02 -04004461
4462 # Pick up the PDK from "values", use it to find the PDK folder, determine
4463 # if it has a "magic" subfolder, and enable/disable the "Edit Layout"
4464 # button accordingly
4465
4466 svalues = selection['values']
emayecsc707a0a2021-08-06 17:13:27 -04004467 #print("svalues :"+str(svalues))
emayecs5966a532021-07-29 10:07:02 -04004468 pdkitems = svalues[1].split()
4469 pdkdir = ''
4470
4471 ef_style=False
4472
4473 if os.path.exists(svalues[0] + '/.config'):
4474 pdkdir = svalues[0] + '/.config/techdir'
4475 elif os.path.exists(svalues[0] + '/.ef-config'):
4476 pdkdir = svalues[0] + '/.ef-config/techdir'
4477 ef_style=True
4478
4479 if pdkdir == '':
4480 print('No pdkname found; layout editing disabled')
4481 self.toppane.appbar.layout_button.config(state='disabled')
4482 else:
4483 try:
4484 if ef_style:
4485 subf = os.listdir(pdkdir + '/libs.tech/magic/current')
4486 else:
4487 subf = os.listdir(pdkdir + '/libs.tech/magic')
4488 except:
4489 print('PDK ' + pdkname + ' has no layout setup; layout editing disabled')
4490 self.toppane.appbar.layout_button.config(state='disabled')
emayecsc707a0a2021-08-06 17:13:27 -04004491
emayecs5966a532021-07-29 10:07:02 -04004492 # If the selected project directory has a JSON file and netlists in the "spi"
4493 # and "testbench" folders, then enable the "Characterize" button; else disable
4494 # it.
4495 # NOTE: project.json is the preferred name for the datasheet
4496 # file. However, the .spi file, .delib file, etc., all have the name of the
emayecsb2487ae2021-08-05 10:30:13 -04004497 # project from "project_name" in the info.yaml file, which is separate from the datasheet.
emayecs5966a532021-07-29 10:07:02 -04004498
4499 found = False
4500 ppath = selection['values'][0]
emayecsb2487ae2021-08-05 10:30:13 -04004501 yamlname = ppath + '/info.yaml'
4502
4503 if os.path.isfile(yamlname):
4504 # Pull the project_name into local store
4505 with open(yamlname, 'r') as f:
4506 datatop = yaml.safe_load(f)
4507 project_data = datatop['project']
4508 ipname = project_data['project_name']
4509 self.project_name = ipname
4510 else:
4511 print('Setting project ip-name from the project folder name.')
4512 self.project_name = pname
emayecs5966a532021-07-29 10:07:02 -04004513 jsonname = ppath + '/project.json'
emayecs5966a532021-07-29 10:07:02 -04004514 if os.path.isfile(jsonname):
emayecs5966a532021-07-29 10:07:02 -04004515 with open(jsonname, 'r') as f:
4516 datatop = json.load(f)
4517 dsheet = datatop['data-sheet']
emayecs5966a532021-07-29 10:07:02 -04004518 found = True
emayecs5966a532021-07-29 10:07:02 -04004519 # Do not specifically prohibit opening the characterization app if
4520 # there is no schematic or netlist. Otherwise the user is prevented
4521 # even from seeing the electrical parameters. Let the characterization
4522 # tool allow or prohibit simulation based on this.
4523 # if os.path.exists(ppath + '/spi'):
4524 # if os.path.isfile(ppath + '/spi/' + ipname + '.spi'):
4525 # found = True
4526 #
4527 # if found == False and os.path.exists(ppath + '/elec'):
4528 # if os.path.isdir(ppath + '/elec/' + ipname + '.delib'):
4529 # if os.path.isfile(ppath + '/elec/' + ipname + '.delib/' + ipname + '.sch'):
4530 # found = True
4531 else:
4532 # Use 'pname' as the default project name.
4533 print('No characterization file ' + jsonname)
emayecs5966a532021-07-29 10:07:02 -04004534
4535 # If datasheet has physical parameters but not electrical parameters, then it's okay
4536 # for it not to have a testbench directory; it's still valid. However, having
4537 # neither physical nor electrical parameters means there's nothing to characterize.
4538 if found and 'electrical-params' in dsheet and len(dsheet['electrical-params']) > 0:
4539 if not os.path.isdir(ppath + '/testbench'):
4540 print('No testbench directory for eletrical parameter simulation methods.', file=sys.stderr)
4541 found = False
4542 elif found and not 'physical-params' in dsheet:
4543 print('Characterization file defines no characterization tests.', file=sys.stderr)
4544 found = False
4545 elif found and 'physical-params' in dsheet and len(dsheet['physical-params']) == 0:
4546 print('Characterization file defines no characterization tests.', file=sys.stderr)
4547 found = False
4548
4549 if found == True:
4550 self.toppane.appbar.char_button.config(state='enabled')
4551 else:
4552 self.toppane.appbar.char_button.config(state='disabled')
4553
4554 # Warning: temporary hack (Tim, 1/9/2018)
4555 # Pad frame generator is currently limited to the XH035 cells, so if the
4556 # project PDK is not XH035, disable the pad frame button
4557
4558 if len(pdkitems) > 1 and pdkitems[1] == 'EFXH035B':
4559 self.toppane.appbar.padframeCalc_button.config(state='enabled')
4560 else:
4561 self.toppane.appbar.padframeCalc_button.config(state='disabled')
4562
4563# main app. fyi: there's a 2nd/earlier __main__ section for splashscreen
4564if __name__ == '__main__':
emayecs582bc382021-08-13 12:32:12 -04004565 ProjectManager(root)
emayecs5966a532021-07-29 10:07:02 -04004566 if deferLoad:
4567 # Without this, mainloop may find&run very short clock-delayed events BEFORE main form display.
4568 # With it 1st project-load can be scheduled using after-time=0 (needn't tune a delay like 100ms).
4569 root.update_idletasks()
4570 root.mainloop()