siliconcompiler 0.26.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. siliconcompiler/__init__.py +24 -0
  2. siliconcompiler/__main__.py +12 -0
  3. siliconcompiler/_common.py +49 -0
  4. siliconcompiler/_metadata.py +36 -0
  5. siliconcompiler/apps/__init__.py +0 -0
  6. siliconcompiler/apps/_common.py +76 -0
  7. siliconcompiler/apps/sc.py +92 -0
  8. siliconcompiler/apps/sc_dashboard.py +94 -0
  9. siliconcompiler/apps/sc_issue.py +178 -0
  10. siliconcompiler/apps/sc_remote.py +199 -0
  11. siliconcompiler/apps/sc_server.py +39 -0
  12. siliconcompiler/apps/sc_show.py +142 -0
  13. siliconcompiler/apps/smake.py +232 -0
  14. siliconcompiler/checklists/__init__.py +0 -0
  15. siliconcompiler/checklists/oh_tapeout.py +41 -0
  16. siliconcompiler/core.py +3221 -0
  17. siliconcompiler/data/RobotoMono/LICENSE.txt +202 -0
  18. siliconcompiler/data/RobotoMono/RobotoMono-Regular.ttf +0 -0
  19. siliconcompiler/data/heartbeat.v +18 -0
  20. siliconcompiler/data/logo.png +0 -0
  21. siliconcompiler/flowgraph.py +570 -0
  22. siliconcompiler/flows/__init__.py +0 -0
  23. siliconcompiler/flows/_common.py +67 -0
  24. siliconcompiler/flows/asicflow.py +180 -0
  25. siliconcompiler/flows/asictopflow.py +38 -0
  26. siliconcompiler/flows/dvflow.py +86 -0
  27. siliconcompiler/flows/fpgaflow.py +202 -0
  28. siliconcompiler/flows/generate_openroad_rcx.py +66 -0
  29. siliconcompiler/flows/lintflow.py +35 -0
  30. siliconcompiler/flows/screenshotflow.py +51 -0
  31. siliconcompiler/flows/showflow.py +59 -0
  32. siliconcompiler/flows/signoffflow.py +53 -0
  33. siliconcompiler/flows/synflow.py +128 -0
  34. siliconcompiler/fpgas/__init__.py +0 -0
  35. siliconcompiler/fpgas/lattice_ice40.py +42 -0
  36. siliconcompiler/fpgas/vpr_example.py +109 -0
  37. siliconcompiler/issue.py +300 -0
  38. siliconcompiler/libs/__init__.py +0 -0
  39. siliconcompiler/libs/asap7sc7p5t.py +8 -0
  40. siliconcompiler/libs/gf180mcu.py +8 -0
  41. siliconcompiler/libs/nangate45.py +8 -0
  42. siliconcompiler/libs/sky130hd.py +8 -0
  43. siliconcompiler/libs/sky130io.py +8 -0
  44. siliconcompiler/package.py +412 -0
  45. siliconcompiler/pdks/__init__.py +0 -0
  46. siliconcompiler/pdks/asap7.py +8 -0
  47. siliconcompiler/pdks/freepdk45.py +8 -0
  48. siliconcompiler/pdks/gf180.py +8 -0
  49. siliconcompiler/pdks/skywater130.py +8 -0
  50. siliconcompiler/remote/__init__.py +36 -0
  51. siliconcompiler/remote/client.py +891 -0
  52. siliconcompiler/remote/schema.py +106 -0
  53. siliconcompiler/remote/server.py +507 -0
  54. siliconcompiler/remote/server_schema/requests/cancel_job.json +51 -0
  55. siliconcompiler/remote/server_schema/requests/check_progress.json +61 -0
  56. siliconcompiler/remote/server_schema/requests/check_server.json +38 -0
  57. siliconcompiler/remote/server_schema/requests/delete_job.json +51 -0
  58. siliconcompiler/remote/server_schema/requests/get_results.json +48 -0
  59. siliconcompiler/remote/server_schema/requests/remote_run.json +40 -0
  60. siliconcompiler/remote/server_schema/responses/cancel_job.json +18 -0
  61. siliconcompiler/remote/server_schema/responses/check_progress.json +30 -0
  62. siliconcompiler/remote/server_schema/responses/check_server.json +32 -0
  63. siliconcompiler/remote/server_schema/responses/delete_job.json +18 -0
  64. siliconcompiler/remote/server_schema/responses/get_results.json +21 -0
  65. siliconcompiler/remote/server_schema/responses/remote_run.json +25 -0
  66. siliconcompiler/report/__init__.py +13 -0
  67. siliconcompiler/report/html_report.py +74 -0
  68. siliconcompiler/report/report.py +355 -0
  69. siliconcompiler/report/streamlit_report.py +137 -0
  70. siliconcompiler/report/streamlit_viewer.py +944 -0
  71. siliconcompiler/report/summary_image.py +117 -0
  72. siliconcompiler/report/summary_table.py +105 -0
  73. siliconcompiler/report/utils.py +163 -0
  74. siliconcompiler/scheduler/__init__.py +2092 -0
  75. siliconcompiler/scheduler/docker_runner.py +253 -0
  76. siliconcompiler/scheduler/run_node.py +138 -0
  77. siliconcompiler/scheduler/send_messages.py +178 -0
  78. siliconcompiler/scheduler/slurm.py +208 -0
  79. siliconcompiler/scheduler/validation/email_credentials.json +54 -0
  80. siliconcompiler/schema/__init__.py +7 -0
  81. siliconcompiler/schema/schema_cfg.py +4014 -0
  82. siliconcompiler/schema/schema_obj.py +1841 -0
  83. siliconcompiler/schema/utils.py +93 -0
  84. siliconcompiler/sphinx_ext/__init__.py +0 -0
  85. siliconcompiler/sphinx_ext/dynamicgen.py +1006 -0
  86. siliconcompiler/sphinx_ext/schemagen.py +221 -0
  87. siliconcompiler/sphinx_ext/utils.py +166 -0
  88. siliconcompiler/targets/__init__.py +0 -0
  89. siliconcompiler/targets/asap7_demo.py +68 -0
  90. siliconcompiler/targets/asic_demo.py +38 -0
  91. siliconcompiler/targets/fpgaflow_demo.py +47 -0
  92. siliconcompiler/targets/freepdk45_demo.py +59 -0
  93. siliconcompiler/targets/gf180_demo.py +77 -0
  94. siliconcompiler/targets/skywater130_demo.py +70 -0
  95. siliconcompiler/templates/email/general.j2 +66 -0
  96. siliconcompiler/templates/email/summary.j2 +43 -0
  97. siliconcompiler/templates/issue/README.txt +26 -0
  98. siliconcompiler/templates/issue/run.sh +6 -0
  99. siliconcompiler/templates/report/bootstrap.min.css +7 -0
  100. siliconcompiler/templates/report/bootstrap.min.js +7 -0
  101. siliconcompiler/templates/report/bootstrap_LICENSE.md +24 -0
  102. siliconcompiler/templates/report/sc_report.j2 +427 -0
  103. siliconcompiler/templates/slurm/run.sh +9 -0
  104. siliconcompiler/templates/tcl/manifest.tcl.j2 +137 -0
  105. siliconcompiler/tools/__init__.py +0 -0
  106. siliconcompiler/tools/_common/__init__.py +432 -0
  107. siliconcompiler/tools/_common/asic.py +115 -0
  108. siliconcompiler/tools/_common/sdc/sc_constraints.sdc +76 -0
  109. siliconcompiler/tools/_common/tcl/sc_pin_constraints.tcl +63 -0
  110. siliconcompiler/tools/bambu/bambu.py +32 -0
  111. siliconcompiler/tools/bambu/convert.py +77 -0
  112. siliconcompiler/tools/bluespec/bluespec.py +40 -0
  113. siliconcompiler/tools/bluespec/convert.py +103 -0
  114. siliconcompiler/tools/builtin/_common.py +155 -0
  115. siliconcompiler/tools/builtin/builtin.py +26 -0
  116. siliconcompiler/tools/builtin/concatenate.py +85 -0
  117. siliconcompiler/tools/builtin/join.py +27 -0
  118. siliconcompiler/tools/builtin/maximum.py +46 -0
  119. siliconcompiler/tools/builtin/minimum.py +57 -0
  120. siliconcompiler/tools/builtin/mux.py +70 -0
  121. siliconcompiler/tools/builtin/nop.py +38 -0
  122. siliconcompiler/tools/builtin/verify.py +83 -0
  123. siliconcompiler/tools/chisel/SCDriver.scala +10 -0
  124. siliconcompiler/tools/chisel/build.sbt +27 -0
  125. siliconcompiler/tools/chisel/chisel.py +37 -0
  126. siliconcompiler/tools/chisel/convert.py +140 -0
  127. siliconcompiler/tools/execute/exec_input.py +41 -0
  128. siliconcompiler/tools/execute/execute.py +17 -0
  129. siliconcompiler/tools/genfasm/bitstream.py +61 -0
  130. siliconcompiler/tools/genfasm/genfasm.py +40 -0
  131. siliconcompiler/tools/ghdl/convert.py +87 -0
  132. siliconcompiler/tools/ghdl/ghdl.py +41 -0
  133. siliconcompiler/tools/icarus/compile.py +87 -0
  134. siliconcompiler/tools/icarus/icarus.py +36 -0
  135. siliconcompiler/tools/icepack/bitstream.py +20 -0
  136. siliconcompiler/tools/icepack/icepack.py +43 -0
  137. siliconcompiler/tools/klayout/export.py +117 -0
  138. siliconcompiler/tools/klayout/klayout.py +119 -0
  139. siliconcompiler/tools/klayout/klayout_export.py +205 -0
  140. siliconcompiler/tools/klayout/klayout_operations.py +363 -0
  141. siliconcompiler/tools/klayout/klayout_show.py +242 -0
  142. siliconcompiler/tools/klayout/klayout_utils.py +176 -0
  143. siliconcompiler/tools/klayout/operations.py +194 -0
  144. siliconcompiler/tools/klayout/screenshot.py +98 -0
  145. siliconcompiler/tools/klayout/show.py +101 -0
  146. siliconcompiler/tools/magic/drc.py +49 -0
  147. siliconcompiler/tools/magic/extspice.py +19 -0
  148. siliconcompiler/tools/magic/magic.py +85 -0
  149. siliconcompiler/tools/magic/sc_drc.tcl +96 -0
  150. siliconcompiler/tools/magic/sc_extspice.tcl +54 -0
  151. siliconcompiler/tools/magic/sc_magic.tcl +47 -0
  152. siliconcompiler/tools/montage/montage.py +30 -0
  153. siliconcompiler/tools/montage/tile.py +66 -0
  154. siliconcompiler/tools/netgen/count_lvs.py +132 -0
  155. siliconcompiler/tools/netgen/lvs.py +90 -0
  156. siliconcompiler/tools/netgen/netgen.py +36 -0
  157. siliconcompiler/tools/netgen/sc_lvs.tcl +46 -0
  158. siliconcompiler/tools/nextpnr/apr.py +24 -0
  159. siliconcompiler/tools/nextpnr/nextpnr.py +59 -0
  160. siliconcompiler/tools/openfpgaloader/openfpgaloader.py +39 -0
  161. siliconcompiler/tools/openroad/__init__.py +0 -0
  162. siliconcompiler/tools/openroad/cts.py +45 -0
  163. siliconcompiler/tools/openroad/dfm.py +66 -0
  164. siliconcompiler/tools/openroad/export.py +131 -0
  165. siliconcompiler/tools/openroad/floorplan.py +70 -0
  166. siliconcompiler/tools/openroad/openroad.py +977 -0
  167. siliconcompiler/tools/openroad/physyn.py +27 -0
  168. siliconcompiler/tools/openroad/place.py +41 -0
  169. siliconcompiler/tools/openroad/rcx_bench.py +95 -0
  170. siliconcompiler/tools/openroad/rcx_extract.py +34 -0
  171. siliconcompiler/tools/openroad/route.py +45 -0
  172. siliconcompiler/tools/openroad/screenshot.py +60 -0
  173. siliconcompiler/tools/openroad/scripts/sc_apr.tcl +499 -0
  174. siliconcompiler/tools/openroad/scripts/sc_cts.tcl +64 -0
  175. siliconcompiler/tools/openroad/scripts/sc_dfm.tcl +20 -0
  176. siliconcompiler/tools/openroad/scripts/sc_export.tcl +98 -0
  177. siliconcompiler/tools/openroad/scripts/sc_floorplan.tcl +413 -0
  178. siliconcompiler/tools/openroad/scripts/sc_metrics.tcl +158 -0
  179. siliconcompiler/tools/openroad/scripts/sc_physyn.tcl +7 -0
  180. siliconcompiler/tools/openroad/scripts/sc_place.tcl +84 -0
  181. siliconcompiler/tools/openroad/scripts/sc_procs.tcl +423 -0
  182. siliconcompiler/tools/openroad/scripts/sc_rcx.tcl +63 -0
  183. siliconcompiler/tools/openroad/scripts/sc_rcx_bench.tcl +20 -0
  184. siliconcompiler/tools/openroad/scripts/sc_rcx_extract.tcl +12 -0
  185. siliconcompiler/tools/openroad/scripts/sc_route.tcl +133 -0
  186. siliconcompiler/tools/openroad/scripts/sc_screenshot.tcl +21 -0
  187. siliconcompiler/tools/openroad/scripts/sc_write.tcl +5 -0
  188. siliconcompiler/tools/openroad/scripts/sc_write_images.tcl +361 -0
  189. siliconcompiler/tools/openroad/show.py +94 -0
  190. siliconcompiler/tools/openroad/templates/pex.tcl +8 -0
  191. siliconcompiler/tools/opensta/__init__.py +101 -0
  192. siliconcompiler/tools/opensta/report_libraries.py +28 -0
  193. siliconcompiler/tools/opensta/scripts/sc_procs.tcl +47 -0
  194. siliconcompiler/tools/opensta/scripts/sc_report_libraries.tcl +74 -0
  195. siliconcompiler/tools/opensta/scripts/sc_timing.tcl +268 -0
  196. siliconcompiler/tools/opensta/timing.py +214 -0
  197. siliconcompiler/tools/slang/__init__.py +49 -0
  198. siliconcompiler/tools/slang/lint.py +101 -0
  199. siliconcompiler/tools/surelog/__init__.py +123 -0
  200. siliconcompiler/tools/surelog/parse.py +183 -0
  201. siliconcompiler/tools/surelog/templates/output.v +7 -0
  202. siliconcompiler/tools/sv2v/convert.py +46 -0
  203. siliconcompiler/tools/sv2v/sv2v.py +37 -0
  204. siliconcompiler/tools/template/template.py +125 -0
  205. siliconcompiler/tools/verilator/compile.py +139 -0
  206. siliconcompiler/tools/verilator/lint.py +19 -0
  207. siliconcompiler/tools/verilator/parse.py +27 -0
  208. siliconcompiler/tools/verilator/verilator.py +172 -0
  209. siliconcompiler/tools/vivado/__init__.py +7 -0
  210. siliconcompiler/tools/vivado/bitstream.py +21 -0
  211. siliconcompiler/tools/vivado/place.py +21 -0
  212. siliconcompiler/tools/vivado/route.py +21 -0
  213. siliconcompiler/tools/vivado/scripts/sc_bitstream.tcl +6 -0
  214. siliconcompiler/tools/vivado/scripts/sc_place.tcl +2 -0
  215. siliconcompiler/tools/vivado/scripts/sc_route.tcl +4 -0
  216. siliconcompiler/tools/vivado/scripts/sc_run.tcl +45 -0
  217. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +25 -0
  218. siliconcompiler/tools/vivado/syn_fpga.py +20 -0
  219. siliconcompiler/tools/vivado/vivado.py +147 -0
  220. siliconcompiler/tools/vpr/_json_constraint.py +63 -0
  221. siliconcompiler/tools/vpr/_xml_constraint.py +109 -0
  222. siliconcompiler/tools/vpr/place.py +137 -0
  223. siliconcompiler/tools/vpr/route.py +124 -0
  224. siliconcompiler/tools/vpr/screenshot.py +54 -0
  225. siliconcompiler/tools/vpr/show.py +88 -0
  226. siliconcompiler/tools/vpr/vpr.py +357 -0
  227. siliconcompiler/tools/xyce/xyce.py +36 -0
  228. siliconcompiler/tools/yosys/lec.py +56 -0
  229. siliconcompiler/tools/yosys/prepareLib.py +59 -0
  230. siliconcompiler/tools/yosys/sc_lec.tcl +84 -0
  231. siliconcompiler/tools/yosys/sc_syn.tcl +79 -0
  232. siliconcompiler/tools/yosys/syn_asic.py +565 -0
  233. siliconcompiler/tools/yosys/syn_asic.tcl +377 -0
  234. siliconcompiler/tools/yosys/syn_asic_fpga_shared.tcl +31 -0
  235. siliconcompiler/tools/yosys/syn_fpga.py +146 -0
  236. siliconcompiler/tools/yosys/syn_fpga.tcl +233 -0
  237. siliconcompiler/tools/yosys/syn_strategies.tcl +81 -0
  238. siliconcompiler/tools/yosys/techmaps/lcu_kogge_stone.v +39 -0
  239. siliconcompiler/tools/yosys/templates/abc.const +2 -0
  240. siliconcompiler/tools/yosys/yosys.py +147 -0
  241. siliconcompiler/units.py +259 -0
  242. siliconcompiler/use.py +177 -0
  243. siliconcompiler/utils/__init__.py +423 -0
  244. siliconcompiler/utils/asic.py +158 -0
  245. siliconcompiler/utils/showtools.py +25 -0
  246. siliconcompiler-0.26.5.dist-info/LICENSE +190 -0
  247. siliconcompiler-0.26.5.dist-info/METADATA +195 -0
  248. siliconcompiler-0.26.5.dist-info/RECORD +251 -0
  249. siliconcompiler-0.26.5.dist-info/WHEEL +5 -0
  250. siliconcompiler-0.26.5.dist-info/entry_points.txt +12 -0
  251. siliconcompiler-0.26.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,891 @@
1
+ # Copyright 2020 Silicon Compiler Authors. All Rights Reserved.
2
+
3
+ import json
4
+ import os
5
+ import requests
6
+ import shutil
7
+ import time
8
+ import urllib.parse
9
+ import tarfile
10
+ import tempfile
11
+ import multiprocessing
12
+
13
+ from siliconcompiler import utils, SiliconCompilerError
14
+ from siliconcompiler import NodeStatus as SCNodeStatus
15
+ from siliconcompiler._metadata import default_server
16
+ from siliconcompiler.flowgraph import nodes_to_execute
17
+ from siliconcompiler.remote import JobStatus
18
+
19
+ # Step name to use while logging
20
+ remote_step_name = 'remote'
21
+
22
+ # Client / server timeout
23
+ __timeout = 10
24
+
25
+ # Generate warning if no server is configured
26
+ __warn_if_no_server = True
27
+
28
+ # Multiprocessing interface.
29
+ multiprocessor = multiprocessing.get_context('spawn')
30
+
31
+ __tos_str = '''Please review the SiliconCompiler cloud's terms of service:
32
+
33
+ https://www.siliconcompiler.com/terms
34
+
35
+ In particular, please ensure that you have the right to distribute any IP
36
+ which is contained in designs that you upload to the service. This public
37
+ service, provided by SiliconCompiler, is not intended to process proprietary IP.
38
+ '''
39
+
40
+
41
+ ###################################
42
+ def get_base_url(chip):
43
+ '''Helper method to get the root URL for API calls, given a Chip object.
44
+ '''
45
+
46
+ rcfg = get_remote_config(chip, False)
47
+ remote_host = rcfg['address']
48
+ if 'port' in rcfg:
49
+ remote_port = rcfg['port']
50
+ else:
51
+ remote_port = 443
52
+ remote_host += ':' + str(remote_port)
53
+ if remote_host.startswith('http'):
54
+ remote_protocol = ''
55
+ else:
56
+ remote_protocol = 'https://' if str(remote_port) == '443' else 'http://'
57
+ return remote_protocol + remote_host
58
+
59
+
60
+ def get_remote_manifest(chip):
61
+ return f'{chip.getworkdir()}/sc_remote.pkg.json'
62
+
63
+
64
+ ###################################
65
+ def __post(chip, url, post_action, success_action, error_action=None):
66
+ '''
67
+ Helper function to handle the post request
68
+ '''
69
+ redirect_url = urllib.parse.urljoin(get_base_url(chip), url)
70
+
71
+ timeouts = 0
72
+ while redirect_url:
73
+ try:
74
+ resp = post_action(redirect_url)
75
+ except requests.Timeout:
76
+ timeouts += 1
77
+ if timeouts > 10:
78
+ raise SiliconCompilerError('Server communications timed out', chip=chip)
79
+ time.sleep(10)
80
+ continue
81
+ except Exception as e:
82
+ raise SiliconCompilerError(f'Server communications error: {e}', chip=chip)
83
+
84
+ code = resp.status_code
85
+ if 200 <= code and code < 300:
86
+ return success_action(resp)
87
+
88
+ try:
89
+ msg_json = resp.json()
90
+ if 'message' in msg_json:
91
+ msg = msg_json['message']
92
+ else:
93
+ msg = resp.text
94
+ except requests.JSONDecodeError:
95
+ msg = resp.text
96
+
97
+ if 300 <= code and code < 400:
98
+ if 'Location' in resp.headers:
99
+ redirect_url = resp.headers['Location']
100
+ continue
101
+
102
+ if error_action:
103
+ return error_action(code, msg)
104
+ else:
105
+ raise SiliconCompilerError(f'Server responded with {code}: {msg}', chip=chip)
106
+
107
+
108
+ ###################################
109
+ def __build_post_params(chip, verbose, job_name=None, job_hash=None):
110
+ '''
111
+ Helper function to build the params for the post request
112
+ '''
113
+ # Use authentication if necessary.
114
+ post_params = {}
115
+
116
+ if job_hash:
117
+ post_params['job_hash'] = job_hash
118
+
119
+ if job_name:
120
+ post_params['job_id'] = job_name
121
+
122
+ rcfg = get_remote_config(chip, verbose)
123
+
124
+ if ('username' in rcfg) and ('password' in rcfg) and \
125
+ (rcfg['username']) and (rcfg['password']):
126
+ post_params['username'] = rcfg['username']
127
+ post_params['key'] = rcfg['password']
128
+
129
+ return post_params
130
+
131
+
132
+ ###################################
133
+ def _remote_preprocess(chip):
134
+ '''
135
+ Helper method to run a local import stage for remote jobs.
136
+ '''
137
+
138
+ # Ensure packages with python sources are copied
139
+ for key in chip.allkeys():
140
+ key_type = chip.get(*key, field='type')
141
+
142
+ if 'dir' in key_type or 'file' in key_type:
143
+ for _, step, index in chip.schema._getvals(*key, return_defvalue=False):
144
+ packages = chip.get(*key, field='package', step=step, index=index)
145
+ force_copy = False
146
+ for package in packages:
147
+ if not package:
148
+ continue
149
+ if package.startswith('python://'):
150
+ force_copy = True
151
+ if force_copy:
152
+ chip.set(*key, True, field='copy', step=step, index=index)
153
+
154
+ # Collect inputs into a collection directory only for remote runs, since
155
+ # we need to send inputs up to the server.
156
+ cfg = get_remote_config(chip, False)
157
+ chip.collect(whitelist=cfg.setdefault('directory_whitelist', []))
158
+
159
+
160
+ ###################################
161
+ def _log_truncated_stats(chip, status, nodes_with_status, nodes_to_print):
162
+ '''
163
+ Helper method to log truncated information about flowgraph nodes
164
+ with a given status, on a single line.
165
+ Used to print info about all statuses besides NodeStatus.RUNNING.
166
+ '''
167
+
168
+ num_nodes = len(nodes_with_status)
169
+ if num_nodes > 0:
170
+ nodes_log = f' {status.title()} ({num_nodes}): '
171
+ log_nodes = []
172
+ for i in range(min(nodes_to_print, num_nodes)):
173
+ log_nodes.append(nodes_with_status[i][0])
174
+ if num_nodes > nodes_to_print:
175
+ log_nodes.append('...')
176
+ nodes_log += ', '.join(log_nodes)
177
+ chip.logger.info(nodes_log)
178
+
179
+
180
+ ###################################
181
+ def _process_progress_info(chip, progress_info, nodes_to_print=3):
182
+ '''
183
+ Helper method to log information about a remote run's progress,
184
+ based on information returned from a 'check_progress/' call.
185
+ '''
186
+
187
+ completed = []
188
+ try:
189
+ # Decode response JSON, if possible.
190
+ job_info = json.loads(progress_info['message'])
191
+
192
+ # Sort and store info about the job's progress.
193
+ chip.logger.info("Job is still running. Status:")
194
+
195
+ nodes_to_log = {}
196
+ for node, node_info in job_info.items():
197
+ status = node_info['status']
198
+ nodes_to_log.setdefault(status, []).append((node, node_info))
199
+
200
+ if SCNodeStatus.is_done(status):
201
+ # collect completed
202
+ completed.append(node)
203
+
204
+ nodes_to_log = {key: nodes_to_log[key] for key in sorted(nodes_to_log.keys())}
205
+
206
+ # Log information about the job's progress.
207
+ # To avoid clutter, only log up to N completed/pending nodes, on a single line.
208
+ # Completed, failed, and timed-out flowgraph nodes:
209
+ for stat, nodes in nodes_to_log.items():
210
+ if SCNodeStatus.is_done(stat):
211
+ _log_truncated_stats(chip, stat, nodes, nodes_to_print)
212
+
213
+ # Running / in-progress flowgraph nodes should all be printed:
214
+ for stat, nodes in nodes_to_log.items():
215
+ if SCNodeStatus.is_running(stat):
216
+ chip.logger.info(f' {stat.title()} ({len(nodes)}):')
217
+ for node, node_info in nodes:
218
+ running_log = f" {node}"
219
+ if 'elapsed_time' in node_info:
220
+ running_log += f" ({node_info['elapsed_time']})"
221
+ chip.logger.info(running_log)
222
+
223
+ # Queued and pending flowgraph nodes:
224
+ for stat, nodes in nodes_to_log.items():
225
+ if SCNodeStatus.is_waiting(stat):
226
+ _log_truncated_stats(chip, stat, nodes, nodes_to_print)
227
+ except json.JSONDecodeError:
228
+ chip.logger.info("Job is still running")
229
+ except Exception as e:
230
+ chip.logger.info(f"Job is still running: local error: {e}")
231
+
232
+ return completed
233
+
234
+
235
+ def get_remote_config_file(chip, fail=True):
236
+ if chip.get('option', 'credentials'):
237
+ # Use the provided remote credentials file.
238
+ cfg_file = os.path.abspath(chip.get('option', 'credentials'))
239
+
240
+ if fail and not os.path.isfile(cfg_file):
241
+ # Check if it's a file since its been requested by the user
242
+ raise SiliconCompilerError(
243
+ f'Unable to find the credentials file: {cfg_file}',
244
+ chip=chip)
245
+ else:
246
+ # Use the default config file path.
247
+ cfg_file = utils.default_credentials_file()
248
+
249
+ return cfg_file
250
+
251
+
252
+ def get_remote_config(chip, verbose):
253
+ '''
254
+ Returns the remote credentials
255
+ '''
256
+ cfg_file = get_remote_config_file(chip)
257
+
258
+ remote_cfg = {}
259
+ cfg_dir = os.path.dirname(cfg_file)
260
+ if os.path.isdir(cfg_dir) and os.path.isfile(cfg_file):
261
+ if verbose:
262
+ chip.logger.info(f'Using credentials: {cfg_file}')
263
+ with open(cfg_file, 'r') as cfgf:
264
+ remote_cfg = json.loads(cfgf.read())
265
+ else:
266
+ global __warn_if_no_server
267
+ if __warn_if_no_server:
268
+ if verbose:
269
+ chip.logger.warning('Could not find remote server configuration: '
270
+ f'defaulting to {default_server}')
271
+ __warn_if_no_server = False
272
+ remote_cfg = {
273
+ "address": default_server,
274
+ "directory_whitelist": []
275
+ }
276
+ if 'address' not in remote_cfg:
277
+ raise SiliconCompilerError(
278
+ 'Improperly formatted remote server configuration - '
279
+ 'please run "sc-remote -configure" and enter your server address and '
280
+ 'credentials.', chip=chip)
281
+
282
+ return remote_cfg
283
+
284
+
285
+ def remote_process(chip):
286
+ '''
287
+ Dispatch the Chip to a remote server for processing.
288
+ '''
289
+ should_resume = not chip.get('option', 'clean')
290
+ remote_resume = should_resume and chip.get('record', 'remoteid')
291
+
292
+ # Pre-process: Run an starting nodes locally, and upload the
293
+ # in-progress build directory to the remote server.
294
+ # Data is encrypted if user / key were specified.
295
+ # run remote process
296
+ if chip.get('arg', 'step'):
297
+ raise SiliconCompilerError('Cannot pass [arg,step] parameter into remote flow.', chip=chip)
298
+ if chip.get('arg', 'index'):
299
+ raise SiliconCompilerError('Cannot pass [arg,index] parameter into remote flow.', chip=chip)
300
+ # Only run the pre-process step if the job doesn't already have a remote ID.
301
+ if not remote_resume:
302
+ _remote_preprocess(chip)
303
+
304
+ # Run the job on the remote server, and wait for it to finish.
305
+ # Set logger to indicate remote run
306
+ chip._init_logger(step=remote_step_name, index=None, in_run=True)
307
+ _remote_run(chip)
308
+
309
+ # Restore logger
310
+ chip._init_logger(in_run=True)
311
+
312
+
313
+ ###################################
314
+ def _remote_run(chip):
315
+ '''
316
+ Helper method to run a job stage on a remote compute cluster.
317
+ Note that files will not be copied to the remote stage; typically
318
+ the source files will be copied into the cluster's storage before
319
+ calling this method.
320
+ If the "-remote" parameter was not passed in, this method
321
+ will print a warning and do nothing.
322
+ This method assumes that the given stage should not be skipped,
323
+ because it is called from within the `Chip.run(...)` method.
324
+
325
+ '''
326
+
327
+ # Ask the remote server to start processing the requested step.
328
+ check_interval = _request_remote_run(chip)
329
+
330
+ # Remove the local 'import.tar.gz' archive.
331
+ local_archive = os.path.join(chip.getworkdir(),
332
+ 'import.tar.gz')
333
+ if os.path.isfile(local_archive):
334
+ os.remove(local_archive)
335
+
336
+ # Run the main 'check_progress' loop to monitor job status until it finishes.
337
+ remote_run_loop(chip, check_interval)
338
+
339
+
340
+ ###################################
341
+ def remote_run_loop(chip, check_interval):
342
+ # Wrapper to allow for capturing of Ctrl+C
343
+ try:
344
+ __remote_run_loop(chip, check_interval)
345
+ except KeyboardInterrupt:
346
+ manifest_path = get_remote_manifest(chip)
347
+ reconnect_cmd = f'sc-remote -cfg {manifest_path} -reconnect'
348
+ cancel_cmd = f'sc-remote -cfg {manifest_path} -cancel'
349
+ chip.logger.info('Disconnecting from remote job')
350
+ chip.logger.info(f'To reconnect to this job use: {reconnect_cmd}')
351
+ chip.logger.info(f'To cancel this job use: {cancel_cmd}')
352
+ raise SiliconCompilerError('Job canceled by user keyboard interrupt')
353
+
354
+
355
+ ###################################
356
+ def __remote_run_loop(chip, check_interval):
357
+ # Check the job's progress periodically until it finishes.
358
+ is_busy = True
359
+ all_nodes = {}
360
+ completed = []
361
+ result_procs = []
362
+
363
+ for step, index in nodes_to_execute(chip):
364
+ if SCNodeStatus.is_done(chip.get('record', 'status', step=step, index=index)):
365
+ continue
366
+ all_nodes[f'{step}{index}'] = (step, index)
367
+
368
+ def schedule_download(node):
369
+ node_proc = multiprocessor.Process(target=fetch_results,
370
+ args=(chip, node))
371
+ node_proc.start()
372
+ result_procs.append(node_proc)
373
+ if node is None:
374
+ node = 'final result'
375
+ chip.logger.info(f' {node}')
376
+
377
+ while is_busy:
378
+ time.sleep(check_interval)
379
+ new_completed, is_busy = check_progress(chip)
380
+ nodes_to_fetch = []
381
+ for node in new_completed:
382
+ if node not in completed:
383
+ nodes_to_fetch.append(node)
384
+ completed.append(node)
385
+ if nodes_to_fetch:
386
+ chip.logger.info(' Fetching completed results:')
387
+ for node in nodes_to_fetch:
388
+ schedule_download(node)
389
+
390
+ # Done: try to fetch any node results which still haven't been retrieved.
391
+ chip.logger.info('Remote job completed! Retrieving final results...')
392
+ for node_name in all_nodes:
393
+ if node_name not in completed:
394
+ schedule_download(node_name)
395
+ schedule_download(None)
396
+
397
+ # Make sure all results are fetched before letting the client issue
398
+ # a deletion request.
399
+ for proc in result_procs:
400
+ proc.join()
401
+
402
+ # Read in node manifests
403
+ for step, index in all_nodes.values():
404
+ manifest = os.path.join(chip.getworkdir(step=step, index=index),
405
+ 'outputs',
406
+ f'{chip.design}.pkg.json')
407
+ if os.path.exists(manifest):
408
+ chip.schema.read_journal(manifest)
409
+
410
+ # Un-set the 'remote' option to avoid from/to-based summary/show errors
411
+ chip.unset('option', 'remote')
412
+
413
+
414
+ ###################################
415
+ def check_progress(chip):
416
+ try:
417
+ is_busy_info = is_job_busy(chip)
418
+ is_busy = is_busy_info['busy']
419
+ completed = []
420
+ if is_busy:
421
+ completed = _process_progress_info(chip,
422
+ is_busy_info)
423
+ return completed, is_busy
424
+ except Exception as e:
425
+ # Sometimes an exception is raised if the request library cannot
426
+ # reach the server due to a transient network issue.
427
+ # Retrying ensures that jobs don't break off when the connection drops.
428
+ chip.logger.info(f"Unknown network error encountered: retrying: {e}")
429
+ return [], True
430
+
431
+
432
+ ###################################
433
+ def _request_remote_run(chip):
434
+ '''
435
+ Helper method to make a web request to start a job stage.
436
+ '''
437
+
438
+ remote_resume = not chip.get('option', 'clean') and chip.get('record', 'remoteid')
439
+ # Only package and upload the entry steps if starting a new job.
440
+ if not remote_resume:
441
+ upload_file = tempfile.TemporaryFile(prefix='sc', suffix='remote.tar.gz')
442
+ with tarfile.open(fileobj=upload_file, mode='w:gz') as tar:
443
+ tar.add(chip.getworkdir(), arcname='')
444
+ # Flush file to ensure everything is written
445
+ upload_file.flush()
446
+
447
+ remote_status = _remote_ping(chip)
448
+
449
+ if remote_status['status'] != 'ready':
450
+ raise SiliconCompilerError('Remote server is not available.', chip=chip)
451
+
452
+ __print_tos(chip, remote_status)
453
+
454
+ if 'pre_upload' in remote_status:
455
+ chip.logger.info(remote_status['pre_upload']['message'])
456
+ time.sleep(remote_status['pre_upload']['delay'])
457
+
458
+ # Make the actual request, streaming the bulk data as a multipart file.
459
+ # Redirected POST requests are translated to GETs. This is actually
460
+ # part of the HTTP spec, so we need to manually follow the trail.
461
+ post_params = {
462
+ 'chip_cfg': chip.schema.cfg,
463
+ 'params': __build_post_params(chip,
464
+ False,
465
+ job_hash=chip.get('record', 'remoteid'))
466
+ }
467
+
468
+ post_files = {'params': json.dumps(post_params)}
469
+ if not remote_resume:
470
+ post_files['import'] = upload_file
471
+ upload_file.seek(0)
472
+
473
+ def post_action(url):
474
+ return requests.post(url,
475
+ files=post_files,
476
+ timeout=__timeout)
477
+
478
+ def success_action(resp):
479
+ return resp.json()
480
+
481
+ resp = __post(chip, '/remote_run/', post_action, success_action)
482
+ if not remote_resume:
483
+ upload_file.close()
484
+
485
+ if 'message' in resp and resp['message']:
486
+ chip.logger.info(resp['message'])
487
+ chip.set('record', 'remoteid', resp['job_hash'])
488
+
489
+ manifest = get_remote_manifest(chip)
490
+ chip.write_manifest(manifest)
491
+
492
+ chip.logger.info(f"Your job's reference ID is: {resp['job_hash']}")
493
+
494
+ return remote_status['progress_interval']
495
+
496
+
497
+ ###################################
498
+ def is_job_busy(chip):
499
+ '''
500
+ Helper method to make an async request asking the remote server
501
+ whether a job is busy, or ready to accept a new step.
502
+ Returns True if the job is busy, False if not.
503
+ '''
504
+
505
+ # Make the request and print its response.
506
+ def post_action(url):
507
+ params = __build_post_params(chip,
508
+ False,
509
+ job_hash=chip.get('record', 'remoteid'),
510
+ job_name=chip.get('option', 'jobname'))
511
+ return requests.post(url,
512
+ data=json.dumps(params),
513
+ timeout=__timeout)
514
+
515
+ def error_action(code, msg):
516
+ return {
517
+ 'busy': True,
518
+ 'message': ''
519
+ }
520
+
521
+ def success_action(resp):
522
+ json_response = json.loads(resp.text)
523
+
524
+ if json_response['status'] != JobStatus.RUNNING:
525
+ if json_response['status'] == JobStatus.REJECTED:
526
+ chip.logger.error(f'Job was rejected: {json_response["message"]}')
527
+ elif json_response['status'] != JobStatus.UNKNOWN:
528
+ chip.logger.info(f'Job status: {json_response["status"]}')
529
+ info = {
530
+ 'busy': json_response['status'] == JobStatus.RUNNING,
531
+ 'message': None
532
+ }
533
+
534
+ if isinstance(json_response['message'], str):
535
+ info['message'] = json_response['message']
536
+ else:
537
+ info['message'] = json.dumps(json_response['message'])
538
+ return info
539
+
540
+ info = __post(chip,
541
+ '/check_progress/',
542
+ post_action,
543
+ success_action,
544
+ error_action=error_action)
545
+
546
+ if not info:
547
+ info = {
548
+ 'busy': True,
549
+ 'message': ''
550
+ }
551
+ return info
552
+
553
+
554
+ ###################################
555
+ def cancel_job(chip):
556
+ '''
557
+ Helper method to request that the server cancel an ongoing job.
558
+ '''
559
+
560
+ def post_action(url):
561
+ return requests.post(url,
562
+ data=json.dumps(__build_post_params(
563
+ chip,
564
+ False,
565
+ job_hash=chip.get('record', 'remoteid'))),
566
+ timeout=__timeout)
567
+
568
+ def success_action(resp):
569
+ return json.loads(resp.text)
570
+
571
+ return __post(chip, '/cancel_job/', post_action, success_action)
572
+
573
+
574
+ ###################################
575
+ def delete_job(chip):
576
+ '''
577
+ Helper method to delete a job from shared remote storage.
578
+ '''
579
+
580
+ def post_action(url):
581
+ return requests.post(url,
582
+ data=json.dumps(__build_post_params(
583
+ chip,
584
+ False,
585
+ job_hash=chip.get('record', 'remoteid'))),
586
+ timeout=__timeout)
587
+
588
+ def success_action(resp):
589
+ return resp.text
590
+
591
+ return __post(chip, '/delete_job/', post_action, success_action)
592
+
593
+
594
+ ###################################
595
+ def fetch_results_request(chip, node, results_fd):
596
+ '''
597
+ Helper method to fetch job results from a remote compute cluster.
598
+ Optional 'node' argument fetches results for only the specified
599
+ flowgraph node (e.g. "floorplan0")
600
+
601
+ Returns:
602
+ * 0 if no error was encountered.
603
+ * [response code] if the results could not be retrieved.
604
+ '''
605
+
606
+ # Set the request URL.
607
+ job_hash = chip.get('record', 'remoteid')
608
+
609
+ # Fetch results archive.
610
+ def post_action(url):
611
+ post_params = __build_post_params(chip, False)
612
+ if node:
613
+ post_params['node'] = node
614
+ return requests.post(url,
615
+ data=json.dumps(post_params),
616
+ stream=True,
617
+ timeout=__timeout)
618
+
619
+ def success_action(resp):
620
+ shutil.copyfileobj(resp.raw, results_fd)
621
+ return 0
622
+
623
+ def error_action(code, msg):
624
+ # Results are fetched in parallel, and a failure in one node
625
+ # does not necessarily mean that the whole job failed.
626
+ if node:
627
+ chip.logger.warning(f'Could not fetch results for node: {node}')
628
+ else:
629
+ chip.logger.warning('Could not fetch results for final results.')
630
+ return 404
631
+
632
+ return __post(chip,
633
+ f'/get_results/{job_hash}.tar.gz',
634
+ post_action,
635
+ success_action,
636
+ error_action=error_action)
637
+
638
+
639
+ ###################################
640
+ def fetch_results(chip, node):
641
+ '''
642
+ Helper method to fetch and open job results from a remote compute cluster.
643
+ Optional 'node' argument fetches results for only the specified
644
+ flowgraph node (e.g. "floorplan0")
645
+ '''
646
+
647
+ # Collect local values.
648
+ job_hash = chip.get('record', 'remoteid')
649
+ local_dir = chip.get('option', 'builddir')
650
+
651
+ # Set default results archive path if necessary, and fetch it.
652
+ with tempfile.TemporaryDirectory(prefix=f'sc_{job_hash}_', suffix=f'_{node}') as tmpdir:
653
+ results_path = os.path.join(tmpdir, 'result.tar.gz')
654
+
655
+ with open(results_path, 'wb') as rd:
656
+ results_code = fetch_results_request(chip, node, rd)
657
+
658
+ # Note: the server should eventually delete the results as they age out (~8h), but this will
659
+ # give us a brief period to look at failed results.
660
+ if results_code:
661
+ return
662
+
663
+ # Unzip the results.
664
+ # Unauthenticated jobs get a gzip archive, authenticated jobs get nested archives.
665
+ # So we need to extract and delete those.
666
+ # Archive contents: server-side build directory. Format:
667
+ # [job_hash]/[design]/[job_name]/[step]/[index]/...
668
+ try:
669
+ with tarfile.open(results_path, 'r:gz') as tar:
670
+ tar.extractall(path=tmpdir)
671
+ except tarfile.TarError as e:
672
+ chip.logger.error(f'Failed to extract data from {results_path}: {e}')
673
+ return
674
+
675
+ work_dir = os.path.join(tmpdir, job_hash)
676
+ if os.path.exists(work_dir):
677
+ shutil.copytree(work_dir, local_dir, dirs_exist_ok=True)
678
+ else:
679
+ chip.logger.error(f'Empty file returned from remote for: {node}')
680
+ return
681
+
682
+
683
+ def _remote_ping(chip):
684
+ # Make the request and print its response.
685
+ rcfg = __build_post_params(chip, True)
686
+
687
+ def post_action(url):
688
+ return requests.post(url,
689
+ data=json.dumps(rcfg),
690
+ timeout=__timeout)
691
+
692
+ def success_action(resp):
693
+ return resp.json()
694
+
695
+ response_info = __post(chip, '/check_server/', post_action, success_action)
696
+ if not response_info:
697
+ raise ValueError('Server response is not valid.')
698
+
699
+ return response_info
700
+
701
+
702
+ ###################################
703
+ def __print_tos(chip, response_info):
704
+ # Print terms-of-service message, if the server provides one.
705
+ if 'terms' in response_info and response_info['terms']:
706
+ chip.logger.info('Terms of Service info for this server:')
707
+ for line in response_info['terms'].splitlines():
708
+ if line:
709
+ chip.logger.info(line)
710
+
711
+
712
+ ###################################
713
+ def remote_ping(chip):
714
+ '''
715
+ Helper method to call check_server on server
716
+ '''
717
+
718
+ # Make the request and print its response.
719
+ response_info = _remote_ping(chip)
720
+
721
+ # Print status value.
722
+ server_status = response_info['status']
723
+ chip.logger.info(f'Server status: {server_status}')
724
+ if server_status != 'ready':
725
+ chip.logger.warning(' Status is not "ready", server cannot accept new jobs.')
726
+
727
+ # Print server-side version info.
728
+ version_info = response_info['versions']
729
+ version_suffix = ' version'
730
+ max_name_string_len = max([len(s) for s in version_info.keys()]) + len(version_suffix)
731
+ chip.logger.info('Server software versions:')
732
+ for name, version in version_info.items():
733
+ print_name = f'{name}{version_suffix}'
734
+ chip.logger.info(f' {print_name: <{max_name_string_len}}: {version}')
735
+
736
+ # Print user info if applicable.
737
+ if 'user_info' in response_info:
738
+ user_info = response_info['user_info']
739
+ if ('compute_time' not in user_info) or \
740
+ ('bandwidth_kb' not in user_info):
741
+ chip.logger.info('Error fetching user information from the remote server.')
742
+ raise ValueError(f'Server response is not valid or missing fields: {user_info}')
743
+
744
+ remote_cfg = get_remote_config(chip, False)
745
+ if 'username' in remote_cfg:
746
+ # Print the user's account info, and return.
747
+ chip.logger.info(f'User {remote_cfg["username"]}:')
748
+
749
+ time_remaining = user_info["compute_time"] / 60.0
750
+ bandwidth_remaining = user_info["bandwidth_kb"]
751
+ chip.logger.info(f' Remaining compute time: {(time_remaining):.2f} minutes')
752
+ chip.logger.info(f' Remaining results bandwidth: {bandwidth_remaining} KiB')
753
+
754
+ __print_tos(chip, response_info)
755
+
756
+ # Return the response info in case the caller wants to inspect it.
757
+ return response_info
758
+
759
+
760
+ def configure_server(chip, server=None, port=None, username=None, password=None):
761
+
762
+ def confirm_dialog(message):
763
+ confirmed = False
764
+ while not confirmed:
765
+ oin = input(f'{message} y/N: ')
766
+ if (not oin) or (oin == 'n') or (oin == 'N'):
767
+ return False
768
+ elif (oin == 'y') or (oin == 'Y'):
769
+ return True
770
+ return False
771
+
772
+ default_server_name = urllib.parse.urlparse(default_server).hostname
773
+
774
+ # Find the config file/directory path.
775
+ cfg_file = get_remote_config_file(chip, False)
776
+ cfg_dir = os.path.dirname(cfg_file)
777
+
778
+ # Create directory if it doesn't exist.
779
+ if cfg_dir and not os.path.isdir(cfg_dir):
780
+ os.makedirs(cfg_dir, exist_ok=True)
781
+
782
+ # If an existing config file exists, prompt the user to overwrite it.
783
+ if os.path.isfile(cfg_file):
784
+ if not confirm_dialog('Overwrite existing remote configuration?'):
785
+ return
786
+
787
+ config = {}
788
+
789
+ # If a command-line argument is passed in, use that as a public server address.
790
+ if server:
791
+ srv_addr = server
792
+ chip.logger.info(f'Creating remote configuration file for server: {srv_addr}')
793
+ else:
794
+ # If no arguments were passed in, interactively request credentials from the user.
795
+ srv_addr = input('Remote server address (leave blank to use default server):\n')
796
+ srv_addr = srv_addr.replace(" ", "")
797
+
798
+ if not srv_addr:
799
+ srv_addr = default_server
800
+ chip.logger.info(f'Using {srv_addr} as server')
801
+
802
+ server = urllib.parse.urlparse(srv_addr)
803
+ has_scheme = True
804
+ if not server.hostname:
805
+ # fake add a scheme to the url
806
+ has_scheme = False
807
+ server = urllib.parse.urlparse('https://' + srv_addr)
808
+ if not server.hostname:
809
+ raise ValueError(f'Invalid address provided: {srv_addr}')
810
+
811
+ if has_scheme:
812
+ config['address'] = f'{server.scheme}://{server.hostname}'
813
+ else:
814
+ config['address'] = server.hostname
815
+
816
+ public_server = default_server_name in srv_addr
817
+ if public_server and not confirm_dialog(__tos_str):
818
+ return
819
+
820
+ if server.port is not None:
821
+ config['port'] = server.port
822
+
823
+ if not public_server:
824
+ if username is None:
825
+ username = server.username
826
+ if username is None:
827
+ username = input('Remote username (leave blank for no username):\n')
828
+ username = username.replace(" ", "")
829
+ if password is None:
830
+ password = server.password
831
+ if password is None:
832
+ password = input('Remote password (leave blank for no password):\n')
833
+ password = password.replace(" ", "")
834
+
835
+ if username:
836
+ config['username'] = username
837
+ if password:
838
+ config['password'] = password
839
+
840
+ config['directory_whitelist'] = []
841
+
842
+ # Save the values to the target config file in JSON format.
843
+ with open(cfg_file, 'w') as f:
844
+ f.write(json.dumps(config, indent=4))
845
+
846
+ # Let the user know that we finished successfully.
847
+ chip.logger.info(f'Remote configuration saved to: {cfg_file}')
848
+
849
+
850
+ def configure_whitelist(chip, add, remove):
851
+ try:
852
+ cfg_file = get_remote_config_file(chip)
853
+ except SiliconCompilerError as e:
854
+ chip.logger.error(f'{e}')
855
+
856
+ chip.logger.info(f'Updating credentials: {cfg_file}')
857
+ cfg = get_remote_config(chip, True)
858
+
859
+ if 'directory_whitelist' not in cfg:
860
+ cfg['directory_whitelist'] = []
861
+
862
+ if add:
863
+ for path in add:
864
+ path = os.path.abspath(path)
865
+ chip.logger.info(f'Adding {path}')
866
+ cfg['directory_whitelist'].append(path)
867
+
868
+ if remove:
869
+ for path in remove:
870
+ path = os.path.abspath(path)
871
+ if path in cfg['directory_whitelist']:
872
+ chip.logger.info(f'Removing {path}')
873
+ cfg['directory_whitelist'].remove(path)
874
+
875
+ cfg['directory_whitelist'] = list(set(cfg['directory_whitelist']))
876
+
877
+ # Save the values to the target config file in JSON format.
878
+ with open(cfg_file, 'w') as f:
879
+ f.write(json.dumps(cfg, indent=4))
880
+
881
+
882
+ def configure_print(chip):
883
+ cfg = get_remote_config(chip, True)
884
+
885
+ chip.logger.info(f'Server: {get_base_url(chip)}')
886
+ if 'username' in cfg:
887
+ chip.logger.info(f'Username: {cfg["username"]}')
888
+ if 'directory_whitelist' in cfg and cfg['directory_whitelist']:
889
+ chip.logger.info('Directory whitelist:')
890
+ for path in sorted(cfg['directory_whitelist']):
891
+ chip.logger.info(f' {path}')