- CLI entry point with yargs parsing for speckl-compile <file> --output-dir --bom-format - TypeScript parser with AST types for all SpeckDL primitives - Generator for PROV-O JSON-LD provenance graphs - Generator for CycloneDX v1.6 BOMs - Generator for SPDX 3.0.1 BOMs - E2E test verifying RetryHandler.speck parsing and all output formats All tests pass.
175 lines
No EOL
6.2 KiB
JavaScript
175 lines
No EOL
6.2 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
/**
|
|
* Generate SPDX 3.0.1 JSON-LD for a Speck
|
|
*/
|
|
export function generateSPDX(ast, outputDir) {
|
|
for (const speck of ast.specks) {
|
|
const spdx = createSPDXDocument(speck);
|
|
// Write the SPDX file
|
|
const filename = `${speck.name}.specbom.spdx.json`;
|
|
const filepath = path.join(outputDir, filename);
|
|
fs.writeFileSync(filepath, JSON.stringify(spdx, null, 2));
|
|
console.log(`Generated SPDX BOM: ${filepath}`);
|
|
}
|
|
}
|
|
function createSPDXDocument(speck) {
|
|
const doc = {
|
|
'@context': [
|
|
'https://spdx.github.io/spdx-v3/spdx-context.jsonld',
|
|
{
|
|
speckl: 'https://speckl.scoble.me/ns/v0.2#'
|
|
}
|
|
],
|
|
'@type': 'spdx:Document',
|
|
spdx: {
|
|
'@id': `spdx:${speck.name}`,
|
|
'@type': 'spdx:Snippet',
|
|
name: speck.name,
|
|
spdxVersion: '3.0.1',
|
|
dataLicense: 'CC0-1.0',
|
|
creationInfo: {
|
|
created: new Date().toISOString(),
|
|
createdBy: ['Organization: Speckl', 'tool: speckl-compile-0.2.0']
|
|
}
|
|
},
|
|
builtBy: [],
|
|
relationships: [],
|
|
elements: []
|
|
};
|
|
// Add toolchain from bom block
|
|
for (const member of speck.members) {
|
|
if (member.type === 'bom') {
|
|
if (member.compiler) {
|
|
doc.builtBy.push({
|
|
'@type': 'spdx:Build',
|
|
tool: [
|
|
{
|
|
name: member.compiler.name,
|
|
version: member.compiler.version || '0.2.0'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
if (member.solver) {
|
|
doc.builtBy[0].tool.push({
|
|
name: member.solver.name,
|
|
version: member.solver.version || '4.12.5'
|
|
});
|
|
}
|
|
if (member.runtime) {
|
|
doc.builtBy[0].tool.push({
|
|
name: member.runtime.name,
|
|
version: member.runtime.version || '1.0.0'
|
|
});
|
|
}
|
|
if (member.license) {
|
|
doc.spdx.spdxLicenseConcluded = member.license;
|
|
}
|
|
if (member.hash) {
|
|
// Parse hash format: sha256:hashvalue
|
|
const hashParts = member.hash.split(':');
|
|
if (hashParts.length === 2) {
|
|
doc.spdx.hash = [
|
|
{
|
|
algorithm: `spdx:sha${hashParts[0].replace('sha', '')}`,
|
|
value: hashParts[1]
|
|
}
|
|
];
|
|
}
|
|
}
|
|
}
|
|
// Add author relationship
|
|
if (member.type === 'author') {
|
|
const authorElement = {
|
|
'@type': 'spdx:Person',
|
|
'@id': `spdx:agent:${member.name}`,
|
|
name: member.name,
|
|
email: member.email
|
|
};
|
|
doc.elements.push(authorElement);
|
|
doc.relationships.push({
|
|
from: { '@id': `spdx:${speck.name}` },
|
|
to: { '@id': authorElement['@id'] },
|
|
relationshipType: 'spdx:wasAttributedTo'
|
|
});
|
|
}
|
|
// Add source relationship
|
|
if (member.type === 'source') {
|
|
const sourceElement = {
|
|
'@type': 'spdx:Activity',
|
|
'@id': `spdx:source:${member.kind}:${member.ref || 'unknown'}`,
|
|
name: `${member.kind} - ${member.ref || 'unknown'}`,
|
|
type: member.kind
|
|
};
|
|
doc.elements.push(sourceElement);
|
|
doc.relationships.push({
|
|
from: { '@id': `spdx:${speck.name}` },
|
|
to: { '@id': sourceElement['@id'] },
|
|
relationshipType: 'spdx:wasInformedBy'
|
|
});
|
|
}
|
|
// Add review annotation
|
|
if (member.type === 'review') {
|
|
doc.spdx.annotations = [
|
|
{
|
|
'@type': 'spdx:Annotation',
|
|
annotationType: 'spdx:REVIEW',
|
|
annotator: {
|
|
'@type': 'spdx:Tool',
|
|
name: 'speckl-compile'
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
comment: `Review type: ${member.kind}`
|
|
}
|
|
];
|
|
}
|
|
// Add provenance relationships
|
|
if (member.type === 'provenance') {
|
|
for (const clause of member.clauses) {
|
|
if (clause.type === 'regulation') {
|
|
const regulationElement = {
|
|
'@type': 'spdx:Requirement',
|
|
'@id': `spdx:regulation:${clause.value}`,
|
|
name: clause.value
|
|
};
|
|
doc.elements.push(regulationElement);
|
|
doc.relationships.push({
|
|
from: { '@id': `spdx:${speck.name}` },
|
|
to: { '@id': regulationElement['@id'] },
|
|
relationshipType: 'speckl:regulationReference'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Add derives relationship
|
|
if (member.type === 'derives') {
|
|
doc.relationships.push({
|
|
from: { '@id': `spdx:${speck.name}` },
|
|
to: { '@id': `spdx:speck:${member.from}` },
|
|
relationshipType: 'spdx:derivesFrom'
|
|
});
|
|
}
|
|
// Add satisfies relationship
|
|
if (member.type === 'satisfies') {
|
|
doc.elements.push({
|
|
'@type': 'spdx:Requirement',
|
|
'@id': `spdx:requirement:${member.requirement}`,
|
|
name: member.requirement
|
|
});
|
|
doc.relationships.push({
|
|
from: { '@id': `spdx:${speck.name}` },
|
|
to: { '@id': `spdx:requirement:${member.requirement}` },
|
|
relationshipType: 'speckl:satisfies',
|
|
additionalProperties: member.clause ? [
|
|
{
|
|
name: 'speckl:clause',
|
|
value: member.clause
|
|
}
|
|
] : undefined
|
|
});
|
|
}
|
|
}
|
|
return doc;
|
|
}
|
|
//# sourceMappingURL=spdx.js.map
|