edacation 0.5.0 → 0.6.0
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.
- package/dist/cli/util.d.ts +2 -1
- package/dist/project/configuration.d.ts +184 -1321
- package/dist/project/configuration.js +31 -13
- package/dist/project/configuration.js.map +1 -1
- package/dist/project/flasher.d.ts +9 -0
- package/dist/project/flasher.js +81 -0
- package/dist/project/flasher.js.map +1 -0
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +1 -0
- package/dist/project/index.js.map +1 -1
- package/dist/project/iverilog.js +1 -1
- package/dist/project/iverilog.js.map +1 -1
- package/dist/project/nextpnr.js +14 -3
- package/dist/project/nextpnr.js.map +1 -1
- package/dist/project/project.d.ts +118 -20
- package/dist/project/project.js +441 -42
- package/dist/project/project.js.map +1 -1
- package/dist/project/target.d.ts +1 -0
- package/dist/project/target.js +3 -2
- package/dist/project/target.js.map +1 -1
- package/dist/util.d.ts +3 -2
- package/dist/util.js +5 -1
- package/dist/util.js.map +1 -1
- package/package.json +12 -10
package/dist/project/project.js
CHANGED
|
@@ -9,11 +9,37 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.Project = exports.ProjectOutputFile = exports.ProjectInputFile = void 0;
|
|
12
|
+
exports.Project = exports.ProjectTarget = exports.ProjectOutputFile = exports.ProjectInputFile = void 0;
|
|
13
13
|
const util_js_1 = require("../util.js");
|
|
14
14
|
const configuration_js_1 = require("./configuration.js");
|
|
15
|
+
const devices_js_1 = require("./devices.js");
|
|
16
|
+
const flasher_js_1 = require("./flasher.js");
|
|
17
|
+
const iverilog_js_1 = require("./iverilog.js");
|
|
18
|
+
const nextpnr_js_1 = require("./nextpnr.js");
|
|
19
|
+
const target_js_1 = require("./target.js");
|
|
20
|
+
const yosys_js_1 = require("./yosys.js");
|
|
21
|
+
const guessInputFileType = (filePath) => {
|
|
22
|
+
// *_tb.<hdl_ext> -> testbench
|
|
23
|
+
// *.<pin_cfg_ext> -> pinconfig
|
|
24
|
+
// default: design
|
|
25
|
+
const baseName = filePath.split('/').pop() ?? filePath;
|
|
26
|
+
const lowerBase = baseName.toLowerCase();
|
|
27
|
+
const ext = lowerBase.split('.').pop() ?? '';
|
|
28
|
+
const hdlExts = util_js_1.FILE_EXTENSIONS_HDL.map(e => e.toLowerCase());
|
|
29
|
+
const pincfgExts = util_js_1.FILE_EXTENSIONS_PINCFG.map(e => e.toLowerCase());
|
|
30
|
+
if (hdlExts.some(e => lowerBase.endsWith(`_tb.${e}`)))
|
|
31
|
+
return 'testbench';
|
|
32
|
+
if (pincfgExts.includes(ext))
|
|
33
|
+
return 'pinconfig';
|
|
34
|
+
return 'design';
|
|
35
|
+
};
|
|
36
|
+
// Fallback clone to handle Proxy objects (structuredClone throws DataCloneError on Proxy)
|
|
37
|
+
const safeStructuredClone = (value) => {
|
|
38
|
+
return JSON.parse(JSON.stringify(value));
|
|
39
|
+
};
|
|
15
40
|
class ProjectInputFile {
|
|
16
|
-
constructor(_path, _type) {
|
|
41
|
+
constructor(_project, _path, _type) {
|
|
42
|
+
this._project = _project;
|
|
17
43
|
this._path = _path;
|
|
18
44
|
this._type = _type;
|
|
19
45
|
}
|
|
@@ -24,7 +50,10 @@ class ProjectInputFile {
|
|
|
24
50
|
return this._type;
|
|
25
51
|
}
|
|
26
52
|
set type(type) {
|
|
53
|
+
if (this._type === type)
|
|
54
|
+
return;
|
|
27
55
|
this._type = type;
|
|
56
|
+
this._project.triggerInputFilesChanged();
|
|
28
57
|
}
|
|
29
58
|
serialize() {
|
|
30
59
|
return {
|
|
@@ -32,16 +61,15 @@ class ProjectInputFile {
|
|
|
32
61
|
type: this.type
|
|
33
62
|
};
|
|
34
63
|
}
|
|
35
|
-
static deserialize(data, ..._args) {
|
|
36
|
-
// Older versions
|
|
37
|
-
// so we need to migrate if data is a string (single output file).
|
|
64
|
+
static deserialize(project, data, ..._args) {
|
|
65
|
+
// Older versions (<= 0.3.9) stored input files as an array of paths
|
|
38
66
|
if (typeof data === 'string') {
|
|
39
67
|
data = { path: data, type: 'design' };
|
|
40
68
|
}
|
|
41
|
-
return new ProjectInputFile(data.path, data.type);
|
|
69
|
+
return new ProjectInputFile(project, data.path, data.type);
|
|
42
70
|
}
|
|
43
|
-
copy() {
|
|
44
|
-
return ProjectInputFile.deserialize(this.serialize());
|
|
71
|
+
copy(project) {
|
|
72
|
+
return ProjectInputFile.deserialize(project, this.serialize());
|
|
45
73
|
}
|
|
46
74
|
}
|
|
47
75
|
exports.ProjectInputFile = ProjectInputFile;
|
|
@@ -62,7 +90,10 @@ class ProjectOutputFile {
|
|
|
62
90
|
if (id !== null && this._project.getTarget(id) === null) {
|
|
63
91
|
throw new Error(`Invalid target id: ${id}`);
|
|
64
92
|
}
|
|
93
|
+
if (this._targetId === id)
|
|
94
|
+
return;
|
|
65
95
|
this._targetId = id;
|
|
96
|
+
this._project.triggerOutputFilesChanged();
|
|
66
97
|
}
|
|
67
98
|
get target() {
|
|
68
99
|
if (!this._targetId)
|
|
@@ -73,7 +104,10 @@ class ProjectOutputFile {
|
|
|
73
104
|
return this._stale;
|
|
74
105
|
}
|
|
75
106
|
set stale(isStale) {
|
|
107
|
+
if (this._stale === isStale)
|
|
108
|
+
return;
|
|
76
109
|
this._stale = isStale;
|
|
110
|
+
this._project.triggerOutputFilesChanged();
|
|
77
111
|
}
|
|
78
112
|
serialize() {
|
|
79
113
|
return {
|
|
@@ -83,8 +117,7 @@ class ProjectOutputFile {
|
|
|
83
117
|
};
|
|
84
118
|
}
|
|
85
119
|
static deserialize(project, data, ..._args) {
|
|
86
|
-
// Older versions
|
|
87
|
-
// so we need to migrate if data is a string (single output file).
|
|
120
|
+
// Older versions (<= 0.3.12) stored output files as an array of paths
|
|
88
121
|
if (typeof data === 'string') {
|
|
89
122
|
data = { path: data, targetId: null, stale: false };
|
|
90
123
|
}
|
|
@@ -95,21 +128,195 @@ class ProjectOutputFile {
|
|
|
95
128
|
}
|
|
96
129
|
}
|
|
97
130
|
exports.ProjectOutputFile = ProjectOutputFile;
|
|
131
|
+
class ProjectTarget {
|
|
132
|
+
constructor(_project, _data) {
|
|
133
|
+
this._project = _project;
|
|
134
|
+
this._data = _data;
|
|
135
|
+
}
|
|
136
|
+
get id() {
|
|
137
|
+
return this._data.id;
|
|
138
|
+
}
|
|
139
|
+
set id(newId) {
|
|
140
|
+
if (newId === this._data.id)
|
|
141
|
+
return;
|
|
142
|
+
if (this._project.hasTarget(newId)) {
|
|
143
|
+
throw new Error(`Target with ID "${newId}" already exists!`);
|
|
144
|
+
}
|
|
145
|
+
this._data.id = newId;
|
|
146
|
+
this._project.triggerConfigurationChanged();
|
|
147
|
+
}
|
|
148
|
+
get name() {
|
|
149
|
+
return this._data.name;
|
|
150
|
+
}
|
|
151
|
+
set name(newName) {
|
|
152
|
+
if (newName === this._data.name)
|
|
153
|
+
return;
|
|
154
|
+
this._data.name = newName;
|
|
155
|
+
this._project.triggerConfigurationChanged();
|
|
156
|
+
}
|
|
157
|
+
get isActive() {
|
|
158
|
+
const activeTarget = this._project.getActiveTarget();
|
|
159
|
+
return activeTarget?.id === this.id;
|
|
160
|
+
}
|
|
161
|
+
setActive() {
|
|
162
|
+
if (this.isActive)
|
|
163
|
+
return;
|
|
164
|
+
this._project.setActiveTarget(this.id);
|
|
165
|
+
}
|
|
166
|
+
get vendorId() {
|
|
167
|
+
return this._data.vendor;
|
|
168
|
+
}
|
|
169
|
+
get availableVendors() {
|
|
170
|
+
return devices_js_1.VENDORS;
|
|
171
|
+
}
|
|
172
|
+
get vendor() {
|
|
173
|
+
return this.availableVendors[this.vendorId];
|
|
174
|
+
}
|
|
175
|
+
setVendor(vendorId) {
|
|
176
|
+
if (vendorId === this._data.vendor)
|
|
177
|
+
return;
|
|
178
|
+
if (!devices_js_1.VENDORS[vendorId]) {
|
|
179
|
+
throw new Error(`Invalid vendor: ${vendorId}`);
|
|
180
|
+
}
|
|
181
|
+
this._data.vendor = vendorId;
|
|
182
|
+
// Reset family/device/package when changing vendor
|
|
183
|
+
this._data.family = Object.keys(this.availableFamilies)[0];
|
|
184
|
+
this._data.device = Object.keys(this.availableDevices)[0];
|
|
185
|
+
this._data.package = Object.keys(this.availablePackages)[0];
|
|
186
|
+
this._project.triggerConfigurationChanged();
|
|
187
|
+
}
|
|
188
|
+
get familyId() {
|
|
189
|
+
return this._data.family;
|
|
190
|
+
}
|
|
191
|
+
get availableFamilies() {
|
|
192
|
+
return this.vendor?.families || {};
|
|
193
|
+
}
|
|
194
|
+
get family() {
|
|
195
|
+
return this.availableFamilies[this.familyId];
|
|
196
|
+
}
|
|
197
|
+
setFamily(familyId) {
|
|
198
|
+
if (familyId === this._data.family)
|
|
199
|
+
return;
|
|
200
|
+
if (!this.vendor?.families[familyId]) {
|
|
201
|
+
throw new Error(`Invalid family: ${familyId}`);
|
|
202
|
+
}
|
|
203
|
+
this._data.family = familyId;
|
|
204
|
+
// Reset device/package when changing family
|
|
205
|
+
this._data.device = Object.keys(this.availableDevices)[0];
|
|
206
|
+
this._data.package = Object.keys(this.availablePackages)[0];
|
|
207
|
+
this._project.triggerConfigurationChanged();
|
|
208
|
+
}
|
|
209
|
+
get deviceId() {
|
|
210
|
+
return this._data.device;
|
|
211
|
+
}
|
|
212
|
+
get availableDevices() {
|
|
213
|
+
return this.family?.devices || {};
|
|
214
|
+
}
|
|
215
|
+
get device() {
|
|
216
|
+
return this.availableDevices[this.deviceId];
|
|
217
|
+
}
|
|
218
|
+
setDevice(deviceId) {
|
|
219
|
+
if (deviceId === this._data.device)
|
|
220
|
+
return;
|
|
221
|
+
if (!this.family?.devices[deviceId]) {
|
|
222
|
+
throw new Error(`Invalid device: ${deviceId}`);
|
|
223
|
+
}
|
|
224
|
+
this._data.device = deviceId;
|
|
225
|
+
// Reset package when changing device
|
|
226
|
+
this._data.package = Object.keys(this.availablePackages)[0];
|
|
227
|
+
this._project.triggerConfigurationChanged();
|
|
228
|
+
}
|
|
229
|
+
get packageId() {
|
|
230
|
+
return this._data.package;
|
|
231
|
+
}
|
|
232
|
+
get availablePackages() {
|
|
233
|
+
return this.device?.packages.reduce((prev, packageId) => {
|
|
234
|
+
const vendorPackages = devices_js_1.VENDORS[this.vendorId].packages;
|
|
235
|
+
prev[packageId] = vendorPackages[packageId] ?? packageId;
|
|
236
|
+
return prev;
|
|
237
|
+
}, {}) ?? {};
|
|
238
|
+
}
|
|
239
|
+
get package() {
|
|
240
|
+
return this.availablePackages[this.packageId];
|
|
241
|
+
}
|
|
242
|
+
setPackage(packageId) {
|
|
243
|
+
if (packageId === this._data.package)
|
|
244
|
+
return;
|
|
245
|
+
if (!this.device?.packages.includes(packageId)) {
|
|
246
|
+
throw new Error(`Invalid package: ${packageId}`);
|
|
247
|
+
}
|
|
248
|
+
this._data.package = packageId;
|
|
249
|
+
this._project.triggerConfigurationChanged();
|
|
250
|
+
}
|
|
251
|
+
get config() {
|
|
252
|
+
return this._data;
|
|
253
|
+
}
|
|
254
|
+
setConfig(path, value) {
|
|
255
|
+
if (!path.length)
|
|
256
|
+
throw new Error('Path must be a non-empty array');
|
|
257
|
+
let cursor = this._data;
|
|
258
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
259
|
+
const key = path[i];
|
|
260
|
+
const next = cursor[key];
|
|
261
|
+
if (typeof next !== 'object' || next === null) {
|
|
262
|
+
const child = {};
|
|
263
|
+
cursor[key] = child;
|
|
264
|
+
cursor = child;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
cursor = next;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const last = path[path.length - 1];
|
|
271
|
+
if (cursor[last] === value)
|
|
272
|
+
return;
|
|
273
|
+
cursor[last] = value;
|
|
274
|
+
this._project.triggerConfigurationChanged();
|
|
275
|
+
}
|
|
276
|
+
getEffectiveOptions(workerId) {
|
|
277
|
+
if (workerId === 'yosys')
|
|
278
|
+
return (0, yosys_js_1.getYosysOptions)(this._project.getConfiguration(), this.id);
|
|
279
|
+
else if (workerId === 'nextpnr')
|
|
280
|
+
return (0, nextpnr_js_1.getNextpnrOptions)(this._project.getConfiguration(), this.id);
|
|
281
|
+
else if (workerId === 'iverilog')
|
|
282
|
+
return (0, iverilog_js_1.getIVerilogOptions)(this._project.getConfiguration(), this.id);
|
|
283
|
+
else if (workerId === 'flasher')
|
|
284
|
+
return (0, flasher_js_1.getFlasherOptions)(this._project.getConfiguration(), this.id);
|
|
285
|
+
throw new Error(`Worker ID "${String(workerId)}" is not supported.`);
|
|
286
|
+
}
|
|
287
|
+
getEffectiveTextConfig(workerId, configId, generated, parse = target_js_1.defaultParse) {
|
|
288
|
+
return (0, target_js_1.getCombined)(this._project.getConfiguration(), this.id, workerId, configId, generated, parse);
|
|
289
|
+
}
|
|
290
|
+
update(updates) {
|
|
291
|
+
if (updates.id && updates.id !== this.id) {
|
|
292
|
+
if (this._project.hasTarget(updates.id)) {
|
|
293
|
+
throw new Error(`Target with ID "${updates.id}" already exists!`);
|
|
294
|
+
}
|
|
295
|
+
this._data.id = updates.id;
|
|
296
|
+
}
|
|
297
|
+
Object.assign(this._data, safeStructuredClone(updates));
|
|
298
|
+
this._project.triggerConfigurationChanged();
|
|
299
|
+
}
|
|
300
|
+
serialize() {
|
|
301
|
+
return this._data;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.ProjectTarget = ProjectTarget;
|
|
98
305
|
class Project {
|
|
99
306
|
constructor(name, inputFiles = [], outputFiles = [], configuration = configuration_js_1.DEFAULT_CONFIGURATION, eventCallback) {
|
|
100
307
|
this.batchedEvents = new Set();
|
|
101
308
|
this.batchCounter = 0;
|
|
102
309
|
this.name = name;
|
|
103
|
-
this.inputFiles = inputFiles.map((file) => ProjectInputFile.deserialize(file));
|
|
310
|
+
this.inputFiles = inputFiles.map((file) => ProjectInputFile.deserialize(this, file));
|
|
104
311
|
this.outputFiles = outputFiles.map((file) => ProjectOutputFile.deserialize(this, file));
|
|
105
312
|
const config = configuration_js_1.schemaProjectConfiguration.safeParse(configuration);
|
|
106
313
|
if (config.success) {
|
|
107
314
|
this.configuration = config.data;
|
|
108
315
|
}
|
|
109
316
|
else {
|
|
110
|
-
throw new Error(`Failed to parse project configuration: ${config.error.
|
|
317
|
+
throw new Error(`Failed to parse project configuration: ${config.error.message}`);
|
|
111
318
|
}
|
|
112
|
-
// Trigger
|
|
319
|
+
// Trigger any updates that the configuration might want to do
|
|
113
320
|
this.updateConfiguration({});
|
|
114
321
|
// Set event callback LAST to prevent firing events in constructor
|
|
115
322
|
this.eventCallback = eventCallback;
|
|
@@ -117,6 +324,9 @@ class Project {
|
|
|
117
324
|
getName() {
|
|
118
325
|
return this.name;
|
|
119
326
|
}
|
|
327
|
+
setName(name) {
|
|
328
|
+
this.name = name;
|
|
329
|
+
}
|
|
120
330
|
getInputFiles() {
|
|
121
331
|
return this.inputFiles;
|
|
122
332
|
}
|
|
@@ -129,7 +339,8 @@ class Project {
|
|
|
129
339
|
addInputFiles(files) {
|
|
130
340
|
for (const file of files) {
|
|
131
341
|
if (!this.hasInputFile(file.path)) {
|
|
132
|
-
const
|
|
342
|
+
const fileType = file.type ?? guessInputFileType(file.path);
|
|
343
|
+
const inputFile = new ProjectInputFile(this, file.path, fileType);
|
|
133
344
|
this.inputFiles.push(inputFile);
|
|
134
345
|
}
|
|
135
346
|
}
|
|
@@ -190,16 +401,14 @@ class Project {
|
|
|
190
401
|
const target = this.getTarget(targetId);
|
|
191
402
|
if (!target)
|
|
192
403
|
throw new Error(`Target "${targetId}" does not exist!`);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
setTestbenchPath(targetId, testbenchPath) {
|
|
404
|
+
const cfg = target.config;
|
|
405
|
+
if (!cfg.yosys)
|
|
406
|
+
cfg.yosys = {};
|
|
407
|
+
if (!cfg.yosys.options)
|
|
408
|
+
cfg.yosys.options = {};
|
|
409
|
+
cfg.yosys.options.topLevelModule = module;
|
|
410
|
+
}
|
|
411
|
+
setActiveTestbenchPath(targetId, testbenchPath) {
|
|
203
412
|
const testbenchFiles = this.getInputFiles()
|
|
204
413
|
.filter((file) => file.type === 'testbench')
|
|
205
414
|
.map((file) => file.path);
|
|
@@ -208,14 +417,30 @@ class Project {
|
|
|
208
417
|
const target = this.getTarget(targetId);
|
|
209
418
|
if (!target)
|
|
210
419
|
throw new Error(`Target "${targetId}" does not exist!`);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
420
|
+
target.setConfig(['iverilog', 'options', 'testbenchFile'], testbenchPath);
|
|
421
|
+
}
|
|
422
|
+
getActiveTestbenchPath(targetId) {
|
|
423
|
+
const target = this.getTarget(targetId);
|
|
424
|
+
if (!target)
|
|
425
|
+
throw new Error(`Target "${targetId}" does not exist!`);
|
|
426
|
+
return target.getEffectiveOptions('iverilog').testbenchFile;
|
|
427
|
+
}
|
|
428
|
+
setActivePinConfigPath(targetId, pinConfigPath) {
|
|
429
|
+
const pinConfigFiles = this.getInputFiles()
|
|
430
|
+
.filter((file) => file.type === 'pinconfig')
|
|
431
|
+
.map((file) => file.path);
|
|
432
|
+
if (pinConfigPath && !pinConfigFiles.includes(pinConfigPath))
|
|
433
|
+
throw new Error(`Pin config file ${pinConfigPath} is not marked as such!`);
|
|
434
|
+
const target = this.getTarget(targetId);
|
|
435
|
+
if (!target)
|
|
436
|
+
throw new Error(`Target "${targetId}" does not exist!`);
|
|
437
|
+
target.setConfig(['nextpnr', 'options', 'pinConfigFile'], pinConfigPath);
|
|
438
|
+
}
|
|
439
|
+
getActivePinConfigPath(targetId) {
|
|
440
|
+
const target = this.getTarget(targetId);
|
|
441
|
+
if (!target)
|
|
442
|
+
throw new Error(`Target "${targetId}" does not exist!`);
|
|
443
|
+
return target.getEffectiveOptions('nextpnr').pinConfigFile;
|
|
219
444
|
}
|
|
220
445
|
setInputFileType(filePath, type) {
|
|
221
446
|
const file = this.getInputFile(filePath);
|
|
@@ -223,11 +448,64 @@ class Project {
|
|
|
223
448
|
console.warn(`Tried to set file type of missing input file: ${filePath}`);
|
|
224
449
|
return;
|
|
225
450
|
}
|
|
226
|
-
file.type = type;
|
|
451
|
+
file.type = type; // internal setter triggers event; batched by decorator
|
|
452
|
+
}
|
|
453
|
+
getTargets() {
|
|
454
|
+
return this.configuration.targets.map(t => new ProjectTarget(this, t));
|
|
455
|
+
}
|
|
456
|
+
hasTarget(id) {
|
|
457
|
+
return this.configuration.targets.some(t => t.id === id);
|
|
227
458
|
}
|
|
228
459
|
getTarget(id) {
|
|
229
|
-
const
|
|
230
|
-
return
|
|
460
|
+
const t = this.configuration.targets.find(t => t.id === id);
|
|
461
|
+
return t ? new ProjectTarget(this, t) : null;
|
|
462
|
+
}
|
|
463
|
+
getActiveTarget() {
|
|
464
|
+
if (!this.configuration.activeTargetId)
|
|
465
|
+
return null;
|
|
466
|
+
return this.getTarget(this.configuration.activeTargetId);
|
|
467
|
+
}
|
|
468
|
+
setActiveTarget(id) {
|
|
469
|
+
if (id !== null && !this.hasTarget(id)) {
|
|
470
|
+
throw new Error(`Target with ID "${id}" does not exist!`);
|
|
471
|
+
}
|
|
472
|
+
this.configuration.activeTargetId = id ?? undefined;
|
|
473
|
+
}
|
|
474
|
+
addTarget(id, config) {
|
|
475
|
+
if (!id) {
|
|
476
|
+
// Generate a unique ID
|
|
477
|
+
let idx = 1;
|
|
478
|
+
while (this.hasTarget(`target${idx}`))
|
|
479
|
+
idx += 1;
|
|
480
|
+
id = `target${idx}`;
|
|
481
|
+
}
|
|
482
|
+
else if (this.hasTarget(id)) {
|
|
483
|
+
throw new Error(`Target with ID "${id}" already exists!`);
|
|
484
|
+
}
|
|
485
|
+
const newTargetObj = {
|
|
486
|
+
...safeStructuredClone(config || configuration_js_1.DEFAULT_TARGET),
|
|
487
|
+
id
|
|
488
|
+
};
|
|
489
|
+
this.configuration.targets.push(newTargetObj);
|
|
490
|
+
return new ProjectTarget(this, newTargetObj);
|
|
491
|
+
}
|
|
492
|
+
removeTarget(id) {
|
|
493
|
+
// In-place removal to avoid reassigning the targets array reference
|
|
494
|
+
const targetsArr = this.configuration.targets;
|
|
495
|
+
for (let i = targetsArr.length - 1; i >= 0; i--) {
|
|
496
|
+
if (targetsArr[i].id === id)
|
|
497
|
+
targetsArr.splice(i, 1);
|
|
498
|
+
}
|
|
499
|
+
for (const outFile of this.outputFiles) {
|
|
500
|
+
if (outFile.targetId === id)
|
|
501
|
+
outFile.targetId = null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
updateTarget(id, updates) {
|
|
505
|
+
const target = this.getTarget(id);
|
|
506
|
+
if (!target)
|
|
507
|
+
throw new Error(`Target with ID "${id}" does not exist!`);
|
|
508
|
+
target.update(updates);
|
|
231
509
|
}
|
|
232
510
|
getConfiguration() {
|
|
233
511
|
return this.configuration;
|
|
@@ -237,18 +515,29 @@ class Project {
|
|
|
237
515
|
...this.configuration,
|
|
238
516
|
...configuration
|
|
239
517
|
};
|
|
240
|
-
//
|
|
518
|
+
// Remove invalid target references from output files
|
|
241
519
|
for (const outFile of this.outputFiles) {
|
|
242
520
|
if (!outFile.target)
|
|
243
521
|
outFile.targetId = null;
|
|
244
522
|
}
|
|
245
523
|
}
|
|
246
524
|
importFromProject(other, doTriggerEvent = true) {
|
|
247
|
-
this.
|
|
525
|
+
this.name = other.getName();
|
|
526
|
+
this.inputFiles = other.getInputFiles().map((file) => file.copy(this));
|
|
248
527
|
this.outputFiles = other.getOutputFiles().map((file) => file.copy(this));
|
|
249
|
-
this.configuration =
|
|
528
|
+
this.configuration = safeStructuredClone(other.getConfiguration());
|
|
250
529
|
if (doTriggerEvent)
|
|
251
|
-
this.emitEvents('inputFiles', 'outputFiles', 'configuration');
|
|
530
|
+
this.emitEvents('inputFiles', 'outputFiles', 'configuration', 'meta');
|
|
531
|
+
}
|
|
532
|
+
// Public triggers used by file/target objects
|
|
533
|
+
triggerInputFilesChanged() {
|
|
534
|
+
this.emitEvents('inputFiles');
|
|
535
|
+
}
|
|
536
|
+
triggerOutputFilesChanged() {
|
|
537
|
+
this.emitEvents('outputFiles');
|
|
538
|
+
}
|
|
539
|
+
triggerConfigurationChanged() {
|
|
540
|
+
this.emitEvents('configuration');
|
|
252
541
|
}
|
|
253
542
|
emitEvents(...events) {
|
|
254
543
|
for (const event of events)
|
|
@@ -259,6 +548,11 @@ class Project {
|
|
|
259
548
|
// Do not emit events when batching
|
|
260
549
|
if (this.batchCounter > 0)
|
|
261
550
|
return;
|
|
551
|
+
// Make any corrections needed,
|
|
552
|
+
// and add 'configuration' event if any corrections were made
|
|
553
|
+
const didCorrect = this.makeCorrections();
|
|
554
|
+
if (didCorrect)
|
|
555
|
+
this.batchedEvents.add('configuration');
|
|
262
556
|
// Emit new + batched events
|
|
263
557
|
if (this.eventCallback)
|
|
264
558
|
this.eventCallback(this, Array.from(this.batchedEvents));
|
|
@@ -271,18 +565,93 @@ class Project {
|
|
|
271
565
|
this.emitEvents(...events);
|
|
272
566
|
return res;
|
|
273
567
|
}
|
|
568
|
+
ignoreEvents(func) {
|
|
569
|
+
// raise batch counter to ignore events,
|
|
570
|
+
// but don't actually emit them later
|
|
571
|
+
this.batchCounter += 1;
|
|
572
|
+
const res = func();
|
|
573
|
+
this.batchCounter -= 1;
|
|
574
|
+
return res;
|
|
575
|
+
}
|
|
274
576
|
static emitsEvents(...events) {
|
|
275
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
577
|
return function decorator(_target, _propertyKey, descriptor) {
|
|
277
578
|
const originalMethod = descriptor.value;
|
|
278
|
-
if (
|
|
579
|
+
if (typeof originalMethod !== 'function')
|
|
279
580
|
throw new Error('No original method!');
|
|
280
581
|
descriptor.value = function (...args) {
|
|
582
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
281
583
|
return this.batchEvents(() => originalMethod.apply(this, args), ...events);
|
|
282
584
|
};
|
|
283
585
|
return descriptor;
|
|
284
586
|
};
|
|
285
587
|
}
|
|
588
|
+
makeCorrections() {
|
|
589
|
+
return this.ignoreEvents(() => {
|
|
590
|
+
let didChange = false;
|
|
591
|
+
didChange = this.correctTestbenchPath() || didChange;
|
|
592
|
+
didChange = this.correctPinconfigPaths() || didChange;
|
|
593
|
+
didChange = this.correctActiveTarget() || didChange;
|
|
594
|
+
return didChange;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
correctTestbenchPath() {
|
|
598
|
+
const testbenches = this.getInputFiles()
|
|
599
|
+
.filter((file) => file.type == 'testbench')
|
|
600
|
+
.map((file) => file.path);
|
|
601
|
+
let didChange = false;
|
|
602
|
+
for (const target of this.getTargets()) {
|
|
603
|
+
const tbPath = target.getEffectiveOptions('iverilog').testbenchFile;
|
|
604
|
+
if (tbPath && testbenches.includes(tbPath)) {
|
|
605
|
+
// testbench is configured and correct
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
else if (!tbPath && testbenches.length === 0) {
|
|
609
|
+
// no path configured but also no testbenches present, so ok
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const newTb = testbenches.length === 0 ? undefined : testbenches[0];
|
|
613
|
+
this.setActiveTestbenchPath(target.id, newTb);
|
|
614
|
+
didChange = true;
|
|
615
|
+
}
|
|
616
|
+
return didChange;
|
|
617
|
+
}
|
|
618
|
+
correctPinconfigPaths() {
|
|
619
|
+
const pinconfigs = this.getInputFiles()
|
|
620
|
+
.filter((file) => file.type == 'pinconfig')
|
|
621
|
+
.map((file) => file.path);
|
|
622
|
+
let didChange = false;
|
|
623
|
+
for (const target of this.getTargets()) {
|
|
624
|
+
const pcPath = target.getEffectiveOptions('nextpnr').pinConfigFile;
|
|
625
|
+
if (pcPath && pinconfigs.includes(pcPath)) {
|
|
626
|
+
// pinconfig is configured and correct
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
else if (!pcPath && pinconfigs.length === 0) {
|
|
630
|
+
// no path configured but also no testbenches present, so ok
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const newPc = pinconfigs.length === 0 ? undefined : pinconfigs[0];
|
|
634
|
+
this.setActivePinConfigPath(target.id, newPc);
|
|
635
|
+
didChange = true;
|
|
636
|
+
}
|
|
637
|
+
return didChange;
|
|
638
|
+
}
|
|
639
|
+
correctActiveTarget() {
|
|
640
|
+
const activeTarget = this.getActiveTarget();
|
|
641
|
+
if (activeTarget)
|
|
642
|
+
return false; // active target exists, so ok
|
|
643
|
+
const activeTargetId = this.configuration.activeTargetId;
|
|
644
|
+
if (activeTargetId && this.hasTarget(activeTargetId))
|
|
645
|
+
return false; // active target ID is valid, so ok
|
|
646
|
+
// No active target or invalid active target ID, so set to first target (if any)
|
|
647
|
+
if (this.configuration.targets.length > 0) {
|
|
648
|
+
this.configuration.activeTargetId = this.configuration.targets[0].id;
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
this.configuration.activeTargetId = undefined;
|
|
652
|
+
}
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
286
655
|
static serialize(project) {
|
|
287
656
|
return {
|
|
288
657
|
name: project.name,
|
|
@@ -309,6 +678,12 @@ class Project {
|
|
|
309
678
|
}
|
|
310
679
|
}
|
|
311
680
|
exports.Project = Project;
|
|
681
|
+
__decorate([
|
|
682
|
+
Project.emitsEvents('meta'),
|
|
683
|
+
__metadata("design:type", Function),
|
|
684
|
+
__metadata("design:paramtypes", [String]),
|
|
685
|
+
__metadata("design:returntype", void 0)
|
|
686
|
+
], Project.prototype, "setName", null);
|
|
312
687
|
__decorate([
|
|
313
688
|
Project.emitsEvents('inputFiles'),
|
|
314
689
|
__metadata("design:type", Function),
|
|
@@ -350,13 +725,37 @@ __decorate([
|
|
|
350
725
|
__metadata("design:type", Function),
|
|
351
726
|
__metadata("design:paramtypes", [String, String]),
|
|
352
727
|
__metadata("design:returntype", void 0)
|
|
353
|
-
], Project.prototype, "
|
|
728
|
+
], Project.prototype, "setActiveTestbenchPath", null);
|
|
729
|
+
__decorate([
|
|
730
|
+
Project.emitsEvents('configuration'),
|
|
731
|
+
__metadata("design:type", Function),
|
|
732
|
+
__metadata("design:paramtypes", [String, String]),
|
|
733
|
+
__metadata("design:returntype", void 0)
|
|
734
|
+
], Project.prototype, "setActivePinConfigPath", null);
|
|
354
735
|
__decorate([
|
|
355
736
|
Project.emitsEvents('inputFiles'),
|
|
356
737
|
__metadata("design:type", Function),
|
|
357
738
|
__metadata("design:paramtypes", [String, Object]),
|
|
358
739
|
__metadata("design:returntype", void 0)
|
|
359
740
|
], Project.prototype, "setInputFileType", null);
|
|
741
|
+
__decorate([
|
|
742
|
+
Project.emitsEvents('configuration'),
|
|
743
|
+
__metadata("design:type", Function),
|
|
744
|
+
__metadata("design:paramtypes", [Object]),
|
|
745
|
+
__metadata("design:returntype", void 0)
|
|
746
|
+
], Project.prototype, "setActiveTarget", null);
|
|
747
|
+
__decorate([
|
|
748
|
+
Project.emitsEvents('configuration'),
|
|
749
|
+
__metadata("design:type", Function),
|
|
750
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
751
|
+
__metadata("design:returntype", ProjectTarget)
|
|
752
|
+
], Project.prototype, "addTarget", null);
|
|
753
|
+
__decorate([
|
|
754
|
+
Project.emitsEvents('configuration'),
|
|
755
|
+
__metadata("design:type", Function),
|
|
756
|
+
__metadata("design:paramtypes", [String]),
|
|
757
|
+
__metadata("design:returntype", void 0)
|
|
758
|
+
], Project.prototype, "removeTarget", null);
|
|
360
759
|
__decorate([
|
|
361
760
|
Project.emitsEvents('configuration'),
|
|
362
761
|
__metadata("design:type", Function),
|