UNPKG

edacation

Version:

Library and CLI for interacting with Yosys and nextpnr.

778 lines 29.1 kB
"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