edacation
Version:
Library and CLI for interacting with Yosys and nextpnr.
778 lines • 29.1 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Project = exports.ProjectTarget = exports.ProjectOutputFile = exports.ProjectInputFile = void 0;
const path_1 = __importDefault(require("path"));
const util_js_1 = require("../util.js");
const configuration_js_1 = require("./configuration.js");
const devices_js_1 = require("./devices.js");
const flasher_js_1 = require("./flasher.js");
const iverilog_js_1 = require("./iverilog.js");
const nextpnr_js_1 = require("./nextpnr.js");
const target_js_1 = require("./target.js");
const yosys_js_1 = require("./yosys.js");
const guessInputFileType = (filePath) => {
// *_tb.<hdl_ext> -> testbench
// *.<pin_cfg_ext> -> pinconfig
// default: design
const baseName = filePath.split('/').pop() ?? filePath;
const lowerBase = baseName.toLowerCase();
const ext = lowerBase.split('.').pop() ?? '';
const hdlExts = util_js_1.FILE_EXTENSIONS_HDL.map(e => e.toLowerCase());
const pincfgExts = util_js_1.FILE_EXTENSIONS_PINCFG.map(e => e.toLowerCase());
if (hdlExts.some(e => lowerBase.endsWith(`_tb.${e}`)))
return 'testbench';
if (pincfgExts.includes(ext))
return 'pinconfig';
return 'design';
};
// Fallback clone to handle Proxy objects (structuredClone throws DataCloneError on Proxy)
const safeStructuredClone = (value) => {
return JSON.parse(JSON.stringify(value));
};
class ProjectInputFile {
constructor(_project, _path, _type) {
this._project = _project;
this._path = _path;
this._type = _type;
}
get path() {
return this._path;
}
get type() {
return this._type;
}
set type(type) {
if (this._type === type)
return;
this._type = type;
this._project.triggerInputFilesChanged();
}
serialize() {
return {
path: this.path,
type: this.type
};
}
static deserialize(project, data, ..._args) {
// Older versions (<= 0.3.9) stored input files as an array of paths
if (typeof data === 'string') {
data = { path: data, type: 'design' };
}
return new ProjectInputFile(project, data.path, data.type);
}
copy(project) {
return ProjectInputFile.deserialize(project, this.serialize());
}
}
exports.ProjectInputFile = ProjectInputFile;
class ProjectOutputFile {
constructor(_project, _path, _targetId = null, _stale = false) {
this._project = _project;
this._path = _path;
this._targetId = _targetId;
this._stale = _stale;
}
get path() {
return this._path;
}
get targetId() {
return this._targetId;
}
set targetId(id) {
if (id !== null && this._project.getTarget(id) === null) {
throw new Error(`Invalid target id: ${id}`);
}
if (this._targetId === id)
return;
this._targetId = id;
this._project.triggerOutputFilesChanged();
}
get target() {
if (!this._targetId)
return null;
return this._project.getTarget(this._targetId);
}
get stale() {
return this._stale;
}
set stale(isStale) {
if (this._stale === isStale)
return;
this._stale = isStale;
this._project.triggerOutputFilesChanged();
}
serialize() {
return {
path: this.path,
targetId: this.targetId,
stale: this.stale
};
}
static deserialize(project, data, ..._args) {
// Older versions (<= 0.3.12) stored output files as an array of paths
if (typeof data === 'string') {
data = { path: data, targetId: null, stale: false };
}
return new ProjectOutputFile(project, data.path, data.targetId, data.stale);
}
copy(project) {
return ProjectOutputFile.deserialize(project, this.serialize());
}
}
exports.ProjectOutputFile = ProjectOutputFile;
class ProjectTarget {
constructor(_project, _data) {
this._project = _project;
this._data = _data;
}
get id() {
return this._data.id;
}
set id(newId) {
if (newId === this._data.id)
return;
if (this._project.hasTarget(newId)) {
throw new Error(`Target with ID "${newId}" already exists!`);
}
this._data.id = newId;
this._project.triggerConfigurationChanged();
}
get name() {
return this._data.name;
}
set name(newName) {
if (newName === this._data.name)
return;
this._data.name = newName;
this._project.triggerConfigurationChanged();
}
get isActive() {
const activeTarget = this._project.getActiveTarget();
return activeTarget?.id === this.id;
}
setActive() {
if (this.isActive)
return;
this._project.setActiveTarget(this.id);
}
getFile(...parts) {
return (0, target_js_1.getTargetFile)(this.config, path_1.default.join(...parts));
}
get vendorId() {
return this._data.vendor;
}
get availableVendors() {
return devices_js_1.VENDORS;
}
get vendor() {
return this.availableVendors[this.vendorId];
}
setVendor(vendorId) {
if (vendorId === this._data.vendor)
return;
if (!devices_js_1.VENDORS[vendorId]) {
throw new Error(`Invalid vendor: ${vendorId}`);
}
this._data.vendor = vendorId;
// Reset family/device/package when changing vendor
this._data.family = Object.keys(this.availableFamilies)[0];
this._data.device = Object.keys(this.availableDevices)[0];
this._data.package = Object.keys(this.availablePackages)[0];
this._project.triggerConfigurationChanged();
}
get familyId() {
return this._data.family;
}
get availableFamilies() {
return this.vendor?.families || {};
}
get family() {
return this.availableFamilies[this.familyId];
}
setFamily(familyId) {
if (familyId === this._data.family)
return;
if (!this.vendor?.families[familyId]) {
throw new Error(`Invalid family: ${familyId}`);
}
this._data.family = familyId;
// Reset device/package when changing family
this._data.device = Object.keys(this.availableDevices)[0];
this._data.package = Object.keys(this.availablePackages)[0];
this._project.triggerConfigurationChanged();
}
get deviceId() {
return this._data.device;
}
get availableDevices() {
return this.family?.devices || {};
}
get device() {
return this.availableDevices[this.deviceId];
}
setDevice(deviceId) {
if (deviceId === this._data.device)
return;
if (!this.family?.devices[deviceId]) {
throw new Error(`Invalid device: ${deviceId}`);
}
this._data.device = deviceId;
// Reset package when changing device
this._data.package = Object.keys(this.availablePackages)[0];
this._project.triggerConfigurationChanged();
}
get packageId() {
return this._data.package;
}
get availablePackages() {
return this.device?.packages.reduce((prev, packageId) => {
const vendorPackages = devices_js_1.VENDORS[this.vendorId].packages;
prev[packageId] = vendorPackages[packageId] ?? packageId;
return prev;
}, {}) ?? {};
}
get package() {
return this.availablePackages[this.packageId];
}
setPackage(packageId) {
if (packageId === this._data.package)
return;
if (!this.device?.packages.includes(packageId)) {
throw new Error(`Invalid package: ${packageId}`);
}
this._data.package = packageId;
this._project.triggerConfigurationChanged();
}
get config() {
return this._data;
}
setConfig(path, value) {
if (!path.length)
throw new Error('Path must be a non-empty array');
let cursor = this._data;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
const next = cursor[key];
if (typeof next !== 'object' || next === null) {
const child = {};
cursor[key] = child;
cursor = child;
}
else {
cursor = next;
}
}
const last = path[path.length - 1];
if (cursor[last] === value)
return;
cursor[last] = value;
this._project.triggerConfigurationChanged();
}
getEffectiveOptions(workerId) {
if (workerId === 'yosys')
return (0, yosys_js_1.getYosysOptions)(this._project.getConfiguration(), this.id);
else if (workerId === 'nextpnr')
return (0, nextpnr_js_1.getNextpnrOptions)(this._project.getConfiguration(), this.id);
else if (workerId === 'iverilog')
return (0, iverilog_js_1.getIVerilogOptions)(this._project.getConfiguration(), this.id);
else if (workerId === 'flasher')
return (0, flasher_js_1.getFlasherOptions)(this._project.getConfiguration(), this.id);
throw new Error(`Worker ID "${String(workerId)}" is not supported.`);
}
getEffectiveTextConfig(workerId, configId, generated, parse = target_js_1.defaultParse) {
return (0, target_js_1.getCombined)(this._project.getConfiguration(), this.id, workerId, configId, generated, parse);
}
update(updates) {
if (updates.id && updates.id !== this.id) {
if (this._project.hasTarget(updates.id)) {
throw new Error(`Target with ID "${updates.id}" already exists!`);
}
this._data.id = updates.id;
}
Object.assign(this._data, safeStructuredClone(updates));
this._project.triggerConfigurationChanged();
}
serialize() {
return this._data;
}
}
exports.ProjectTarget = ProjectTarget;
class Project {
constructor(name, inputFiles = [], outputFiles = [], configuration = configuration_js_1.DEFAULT_CONFIGURATION, eventCallback) {
this.batchedEvents = new Set();
this.batchCounter = 0;
this.name = name;
this.inputFiles = inputFiles.map((file) => ProjectInputFile.deserialize(this, file));
this.outputFiles = outputFiles.map((file) => ProjectOutputFile.deserialize(this, file));
const config = configuration_js_1.schemaProjectConfiguration.safeParse(configuration);
if (config.success) {
this.configuration = config.data;
}
else {
throw new Error(`Failed to parse project configuration: ${config.error.message}`);
}
// Trigger any updates that the configuration might want to do
this.updateConfiguration({});
// Set event callback LAST to prevent firing events in constructor
this.eventCallback = eventCallback;
}
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
getInputFiles() {
return this.inputFiles;
}
hasInputFile(filePath) {
return this.getInputFile(filePath) !== null;
}
getInputFile(filePath) {
return this.inputFiles.find((file) => file.path === filePath) ?? null;
}
addInputFiles(files) {
for (const file of files) {
if (!this.hasInputFile(file.path)) {
const fileType = file.type ?? guessInputFileType(file.path);
const inputFile = new ProjectInputFile(this, file.path, fileType);
this.inputFiles.push(inputFile);
}
}
this.inputFiles.sort((a, b) => {
return a < b ? -1 : 1;
});
this.expireOutputFiles();
}
removeInputFiles(filePaths) {
this.inputFiles = this.inputFiles.filter((file) => !filePaths.includes(file.path));
this.expireOutputFiles();
}
getOutputFiles() {
return this.outputFiles;
}
hasOutputFile(filePath) {
return this.getOutputFile(filePath) !== null;
}
getOutputFile(filePath) {
return this.outputFiles.find((file) => file.path === filePath) ?? null;
}
addOutputFiles(files) {
for (const file of files) {
const existingOutFile = this.getOutputFile(file.path);
if (existingOutFile) {
// File already exists, so we don't want to add it again.
// But, we should make sure the target ID gets updated and set `stale` to false.
existingOutFile.targetId = file.targetId;
existingOutFile.stale = false;
continue;
}
const outputFile = new ProjectOutputFile(this, file.path, file.targetId);
if (outputFile.target === null)
throw new Error(`Invalid target ID: ${file.targetId}`);
this.outputFiles.push(outputFile);
}
this.outputFiles.sort((a, b) => {
return a < b ? -1 : 1;
});
}
removeOutputFiles(filePaths) {
this.outputFiles = this.outputFiles.filter((file) => !filePaths.includes(file.path));
}
expireOutputFiles() {
if (!this.outputFiles.length)
return;
let didUpdate = false;
for (const file of this.outputFiles) {
if (!file.stale) {
file.stale = true;
didUpdate = true;
}
}
if (didUpdate)
this.emitEvents('outputFiles');
}
setTopLevelModule(targetId, module) {
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
const cfg = target.config;
if (!cfg.yosys)
cfg.yosys = {};
if (!cfg.yosys.options)
cfg.yosys.options = {};
cfg.yosys.options.topLevelModule = module;
}
setActiveTestbenchPath(targetId, testbenchPath) {
const testbenchFiles = this.getInputFiles()
.filter((file) => file.type === 'testbench')
.map((file) => file.path);
if (testbenchPath && !testbenchFiles.includes(testbenchPath))
throw new Error(`Testbench ${testbenchPath} is not marked as such!`);
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
target.setConfig(['iverilog', 'options', 'testbenchFile'], testbenchPath);
}
getActiveTestbenchPath(targetId) {
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
return target.getEffectiveOptions('iverilog').testbenchFile;
}
setActivePinConfigPath(targetId, pinConfigPath) {
const pinConfigFiles = this.getInputFiles()
.filter((file) => file.type === 'pinconfig')
.map((file) => file.path);
if (pinConfigPath && !pinConfigFiles.includes(pinConfigPath))
throw new Error(`Pin config file ${pinConfigPath} is not marked as such!`);
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
target.setConfig(['nextpnr', 'options', 'pinConfigFile'], pinConfigPath);
}
getActivePinConfigPath(targetId) {
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
return target.getEffectiveOptions('nextpnr').pinConfigFile;
}
setInputFileType(filePath, type) {
const file = this.getInputFile(filePath);
if (!file) {
console.warn(`Tried to set file type of missing input file: ${filePath}`);
return;
}
file.type = type; // internal setter triggers event; batched by decorator
}
getTargets() {
return this.configuration.targets.map(t => new ProjectTarget(this, t));
}
hasTarget(id) {
return this.configuration.targets.some(t => t.id === id);
}
getTarget(id) {
const t = this.configuration.targets.find(t => t.id === id);
return t ? new ProjectTarget(this, t) : null;
}
getActiveTarget() {
if (!this.configuration.activeTargetId)
return null;
return this.getTarget(this.configuration.activeTargetId);
}
setActiveTarget(id) {
if (id !== null && !this.hasTarget(id)) {
throw new Error(`Target with ID "${id}" does not exist!`);
}
this.configuration.activeTargetId = id ?? undefined;
}
addTarget(id, config) {
if (!id) {
// Generate a unique ID
let idx = 1;
while (this.hasTarget(`target${idx}`))
idx += 1;
id = `target${idx}`;
}
else if (this.hasTarget(id)) {
throw new Error(`Target with ID "${id}" already exists!`);
}
const newTargetObj = {
...safeStructuredClone(config || configuration_js_1.DEFAULT_TARGET),
id
};
this.configuration.targets.push(newTargetObj);
return new ProjectTarget(this, newTargetObj);
}
removeTarget(id) {
// In-place removal to avoid reassigning the targets array reference
const targetsArr = this.configuration.targets;
for (let i = targetsArr.length - 1; i >= 0; i--) {
if (targetsArr[i].id === id)
targetsArr.splice(i, 1);
}
for (const outFile of this.outputFiles) {
if (outFile.targetId === id)
outFile.targetId = null;
}
}
updateTarget(id, updates) {
const target = this.getTarget(id);
if (!target)
throw new Error(`Target with ID "${id}" does not exist!`);
target.update(updates);
}
getConfiguration() {
return this.configuration;
}
updateConfiguration(configuration) {
this.configuration = {
...this.configuration,
...configuration
};
// Remove invalid target references from output files
for (const outFile of this.outputFiles) {
if (!outFile.target)
outFile.targetId = null;
}
}
importFromProject(other, doTriggerEvent = true) {
this.name = other.getName();
this.inputFiles = other.getInputFiles().map((file) => file.copy(this));
this.outputFiles = other.getOutputFiles().map((file) => file.copy(this));
this.configuration = safeStructuredClone(other.getConfiguration());
if (doTriggerEvent)
this.emitEvents('inputFiles', 'outputFiles', 'configuration', 'meta');
}
// Public triggers used by file/target objects
triggerInputFilesChanged() {
this.emitEvents('inputFiles');
}
triggerOutputFilesChanged() {
this.emitEvents('outputFiles');
}
triggerConfigurationChanged() {
this.emitEvents('configuration');
}
emitEvents(...events) {
for (const event of events)
this.batchedEvents.add(event);
// Do not emit when empty
if (!this.batchedEvents.size)
return;
// Do not emit events when batching
if (this.batchCounter > 0)
return;
// Make any corrections needed,
// and add 'configuration' event if any corrections were made
const didCorrect = this.makeCorrections();
if (didCorrect)
this.batchedEvents.add('configuration');
// Emit new + batched events
if (this.eventCallback)
this.eventCallback(this, Array.from(this.batchedEvents));
this.batchedEvents.clear();
}
batchEvents(func, ...events) {
this.batchCounter += 1;
const res = func();
this.batchCounter -= 1;
this.emitEvents(...events);
return res;
}
ignoreEvents(func) {
// raise batch counter to ignore events,
// but don't actually emit them later
this.batchCounter += 1;
const res = func();
this.batchCounter -= 1;
return res;
}
static emitsEvents(...events) {
return function decorator(_target, _propertyKey, descriptor) {
const originalMethod = descriptor.value;
if (typeof originalMethod !== 'function')
throw new Error('No original method!');
descriptor.value = function (...args) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.batchEvents(() => originalMethod.apply(this, args), ...events);
};
return descriptor;
};
}
makeCorrections() {
return this.ignoreEvents(() => {
let didChange = false;
didChange = this.correctTestbenchPath() || didChange;
didChange = this.correctPinconfigPaths() || didChange;
didChange = this.correctActiveTarget() || didChange;
return didChange;
});
}
correctTestbenchPath() {
const testbenches = this.getInputFiles()
.filter((file) => file.type == 'testbench')
.map((file) => file.path);
let didChange = false;
for (const target of this.getTargets()) {
const tbPath = target.getEffectiveOptions('iverilog').testbenchFile;
if (tbPath && testbenches.includes(tbPath)) {
// testbench is configured and correct
continue;
}
else if (!tbPath && testbenches.length === 0) {
// no path configured but also no testbenches present, so ok
continue;
}
const newTb = testbenches.length === 0 ? undefined : testbenches[0];
this.setActiveTestbenchPath(target.id, newTb);
didChange = true;
}
return didChange;
}
correctPinconfigPaths() {
const pinconfigs = this.getInputFiles()
.filter((file) => file.type == 'pinconfig')
.map((file) => file.path);
let didChange = false;
for (const target of this.getTargets()) {
const pcPath = target.getEffectiveOptions('nextpnr').pinConfigFile;
if (pcPath && pinconfigs.includes(pcPath)) {
// pinconfig is configured and correct
continue;
}
else if (!pcPath && pinconfigs.length === 0) {
// no path configured but also no testbenches present, so ok
continue;
}
const newPc = pinconfigs.length === 0 ? undefined : pinconfigs[0];
this.setActivePinConfigPath(target.id, newPc);
didChange = true;
}
return didChange;
}
correctActiveTarget() {
const activeTarget = this.getActiveTarget();
if (activeTarget)
return false; // active target exists, so ok
const activeTargetId = this.configuration.activeTargetId;
if (activeTargetId && this.hasTarget(activeTargetId))
return false; // active target ID is valid, so ok
// No active target or invalid active target ID, so set to first target (if any)
if (this.configuration.targets.length > 0) {
this.configuration.activeTargetId = this.configuration.targets[0].id;
}
else {
this.configuration.activeTargetId = undefined;
}
return true;
}
static serialize(project) {
return {
name: project.name,
inputFiles: project.inputFiles.map((file) => file.serialize()),
outputFiles: project.outputFiles.map((file) => file.serialize()),
configuration: project.configuration
};
}
static deserialize(data, ..._args) {
const name = data.name;
const inputFiles = data.inputFiles ?? [];
const outputFiles = data.outputFiles ?? [];
const configuration = data.configuration ?? {};
return new Project(name, inputFiles, outputFiles, configuration);
}
static loadFromData(rawData) {
const data = (0, util_js_1.decodeJSON)(rawData);
const project = Project.deserialize(data);
return project;
}
static storeToData(project) {
const data = Project.serialize(project);
return (0, util_js_1.encodeJSON)(data, true);
}
}
exports.Project = Project;
__decorate([
Project.emitsEvents('meta'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], Project.prototype, "setName", null);
__decorate([
Project.emitsEvents('inputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "addInputFiles", null);
__decorate([
Project.emitsEvents('inputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "removeInputFiles", null);
__decorate([
Project.emitsEvents('outputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "addOutputFiles", null);
__decorate([
Project.emitsEvents('outputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "removeOutputFiles", null);
__decorate([
Project.emitsEvents(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], Project.prototype, "expireOutputFiles", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", void 0)
], Project.prototype, "setTopLevelModule", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", void 0)
], Project.prototype, "setActiveTestbenchPath", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", void 0)
], Project.prototype, "setActivePinConfigPath", null);
__decorate([
Project.emitsEvents('inputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "setInputFileType", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "setActiveTarget", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", ProjectTarget)
], Project.prototype, "addTarget", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], Project.prototype, "removeTarget", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "updateConfiguration", null);
__decorate([
Project.emitsEvents(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Project, Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "importFromProject", null);
//# sourceMappingURL=project.js.map