@iitm_shakti/templates
Version:
An xPack with templates to generate Shakti Core Complex projects
463 lines (398 loc) • 14.7 kB
JavaScript
/*
* This file is part of the µOS++ distribution.
* (https://github.com/micro-os-plus)
* Copyright (c) 2017 Liviu Ionescu.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/* eslint valid-jsdoc: "error" */
/* eslint max-len: [ "error", 80, { "ignoreUrls": true } ] */
// ----------------------------------------------------------------------------
/**
* The XpmInitTemplate module.
*
* It is re-exported publicly by `index.js`.
*
* To import classes from this module into Node.js applications, use:
*
* ```javascript
* const XpmInitTemplate = require('./template.js').XpmInitTemplate
* ```
*/
// ----------------------------------------------------------------------------
const path = require('path')
const fs = require('fs')
const readlineSync = require('readline-sync')
const shell = require('shelljs')
// https://www.npmjs.com/package/shopify-liquid
const Liquid = require('liquidjs')
const Promisifier = require('@ilg/es6-promisifier').Promisifier
// ES6: `import { CliCommand, CliExitCodes, CliError, CliErrorApplication }
// from 'cli-start-options'
const CliExitCodes = require('@ilg/cli-start-options').CliExitCodes
const CliError = require('@ilg/cli-start-options').CliError
// ----------------------------------------------------------------------------
// Promisify functions from the Node.js callbacks library.
// New functions have similar names, but suffixed with `Promise`.
Promisifier.promisifyInPlace(fs, 'writeFile')
// ----------------------------------------------------------------------------
// Provided:
// language (c, cpp)
// boardName (Shakti Arty7 C-Class,Shakti Arty E-Class)
// content (empty, blinky)
// syscalls (none/retarget/semihosting)
// trace (none, uart0ftdi)
// useSomeWarnings=true
// useMostWarnings=true
// useWerror=true
// useOg=true
// useNano=true
// Computed:
// fileExtension (c, cpp)
// boardName (carty100T,earty35T)
// deviceMacro <string>
// boardMacro <string>
// boardFolder <string>
// boardDescription <string>
// ============================================================================
// The result is in the properties map:
// context.config.properties[key] = value
// The description is shown when '?' is entered as selection.
const properties = {
language: {
label: 'Programming language',
description: 'Select the preferred programming language',
type: 'select',
items: {
c: 'C for the application files, C and C++ for the system',
// cpp: 'C++ for the application files, C++ and C for the system'
},
default: 'c'
},
boardName: {
label: 'Board',
description: 'Select the Shakti board name',
type: 'select',
items: {
carty100T: 'Shakti Arty7 C-Class',
earty35T: 'Shakti Arty7 E-Class'
},
default: 'carty100T'
},
content: {
label: 'Content',
description: 'Choose the project content',
type: 'select',
items: {
empty: 'Empty (add your own content)',
blinky: 'Blinky (blink one or more LEDs)'
},
default: 'empty'
},
syscalls: {
label: 'Use system calls',
description: 'Control how system calls are implemented',
type: 'select',
items: {
none: 'Free-standing (no POSIX system calls)',
retarget: 'POSIX (system calls implemented by application code)',
semihosting: 'Semihosting (POSIX system calls via host)'
},
default: 'none'
},
trace: {
label: 'Trace output',
description: 'Control where the trace output messages are forwarded',
type: 'select',
items: {
none: 'None (no trace output)',
uart0ftdi: 'UART0 (via FTDI)',
stdout: 'Semihosting STDOUT stream',
debug: 'Semihosting DEBUG channel'
},
default: 'uart0ftdi'
},
useSomeWarnings: {
label: 'Check some warnings',
description: 'Enable -Wall and -Wextra to catch most common warnings',
type: 'boolean',
default: false
},
useMostWarnings: {
label: 'Check most warnings',
description: 'Enable as many warnings as possible',
type: 'boolean',
default: false
},
useWerror: {
label: 'Enable -Werror',
description: 'Instruct the compiler to stop on warnings',
type: 'boolean',
default: false
},
useOg: {
label: 'Use -Og on debug',
description: 'Use the new optimization flag for the debug configurations',
type: 'boolean',
default: false
},
useNano: {
label: 'Use newlib nano',
description: 'Use the size optimised version of newlib',
type: 'boolean',
default: true
}
}
// ============================================================================
// export
class XpmInitTemplate {
// --------------------------------------------------------------------------
constructor (context) {
this.context = context
this.log = context.log
}
async run () {
const log = this.log
log.trace(`${this.constructor.name}.run()`)
log.info()
const context = this.context
const config = context.config
this.isInteractive = false
// Properties may have been set by --property name=value.
if (!config.properties['boardName']) {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
log.error('Missing board name.')
// this.help()
return CliExitCodes.ERROR.SYNTAX // No project name.
}
this.enterInteractiveMode(properties)
// Reset start time to skip interactive time.
context.startTime = Date.now()
this.isInteractive = true
console.log()
} else {
let isError = false
for (const [key, val] of Object.entries(config.properties)) {
const value = this.validateInput(properties, key, val)
if (value === undefined) {
log.error(`Unsupported property '${key}'`)
isError = true
}
if (value === null) {
log.error(`Unsupported value for '${key}=${val}'`)
isError = true
}
}
if (isError) {
return CliExitCodes.ERROR.SYNTAX
}
// Add defaults for missing properties
for (const [key, val] of Object.entries(properties)) {
if (!config.properties[key]) {
config.properties[key] = val.default
}
}
}
// At this point context.config.properties have full data,
// including defaults.
const liquidMap = Object.assign({}, config.properties)
liquidMap['projectName'] = config.projectName
switch (liquidMap['boardName']) {
case 'carty100T':
liquidMap['deviceName'] = 'carty100T'
liquidMap['deviceMacro'] = 'SHAKTI_CARTY100T'
liquidMap['boardMacro'] = 'SHAKTI_CARTY100T_BOARD'
liquidMap['boardFolder'] = 'shakti-arty-boards'
liquidMap['boardDescription'] = 'Shakti C-Arty100T'
liquidMap['march'] = 'rv64imafdc'
liquidMap['mabi'] = 'lp64d'
break
case 'earty35T':
liquidMap['deviceName'] = 'earty35T'
liquidMap['deviceMacro'] = 'SHAKTI_EARTY35T'
liquidMap['boardMacro'] = 'SHAKTI_EARTY35T_BOARD'
liquidMap['boardFolder'] = 'shakti-arty-boards'
liquidMap['boardDescription'] = 'Shakti E-Arty35T'
liquidMap['march'] = 'rv32imac'
liquidMap['mabi'] = 'ilp32'
break
default:
log.error(`Unsupported board '${liquidMap['boardName']}'.`)
return CliExitCodes.ERROR.SYNTAX
}
liquidMap['fileExtension'] = liquidMap['language']
const currentTime = new Date()
liquidMap['year'] = currentTime.getFullYear().toString()
liquidMap['authorName'] = '<your-name-here>'
this.liquidMap = liquidMap
await this.generate()
return CliExitCodes.SUCCESS
}
validateInput (properties, name, value) {
const propDef = properties[name]
if (!propDef) {
return undefined
}
if (propDef.type === 'select') {
if (propDef.items[value]) {
return value
}
} else if (propDef.type === 'boolean') {
if (value === 'true') {
return true
} else if (value === 'false') {
return false
}
}
if (value === '') {
return propDef.default
}
return null
}
enterInteractiveMode (properties) {
const context = this.context
const config = context.config
for (const [key, val] of Object.entries(properties)) {
let out = `${val.label}?`
if (val.type === 'select') {
out += ' (' + Object.keys(val.items).join(', ') + ', ?)'
} else if (val.type === 'boolean') {
out += ' (true, false, ?)'
}
if (val.default !== undefined) {
out += ` [${val.default}]`
}
out += ': '
while (true) {
let answer = readlineSync.question(out)
// No need for more trimming
answer = this.validateInput(properties, key, answer)
if (answer != null && answer !== undefined) {
config.properties[key] = answer
break
}
console.log(val.description)
if (val.type === 'select') {
for (const ival of Object.values(val.items)) {
console.log(`- ${ival}`)
}
}
}
}
}
async render (inputFileRelativePath, outputFileRelativePath, map) {
const log = this.log
const str = await this.engine.renderFile(inputFileRelativePath, map)
// const headerPath = path.resolve(codePath, `${pnam}.h`)
try {
await fs.writeFilePromise(outputFileRelativePath, str, 'utf8')
} catch (err) {
throw new CliError(err.message, CliExitCodes.ERROR.OUTPUT)
}
log.info(`File '${outputFileRelativePath}' generated.`)
}
async generate () {
const log = this.log
// const context = this.context
const liquidMap = this.liquidMap
const lang = (liquidMap['language'] === 'cpp') ? 'C++' : 'C'
log.info(`Creating the ${lang} project '${liquidMap['projectName']}'...`)
if (!this.isInteractive) {
log.info(`- boardName=${liquidMap['boardName']}`)
log.info(`- content=${liquidMap['content']}`)
log.info(`- syscalls=${liquidMap['syscalls']}`)
log.info(`- trace=${liquidMap['trace']}`)
log.info(`- useSomeWarnings=${liquidMap['useSomeWarnings']}`)
log.info(`- useMostWarnings=${liquidMap['useMostWarnings']}`)
log.info(`- useWerror=${liquidMap['useWerror']}`)
log.info(`- useOg=${liquidMap['useOg']}`)
log.info(`- useNano=${liquidMap['useNano']}`)
log.info()
}
const templatesPath = path.resolve(__dirname, '..', 'assets', 'sources')
log.debug(`from='${templatesPath}'`)
this.engine = Liquid({
root: templatesPath,
cache: false,
strict_filters: true, // default: false
strict_variables: true, // default: false
trim_right: false, // default: false
trim_left: false // default: false
})
const fileExtension = liquidMap['fileExtension']
const boardName = liquidMap['boardName']
// ------------------------------------------------------------------------
// Generate the application files.
await this.render('LICENSE.liquid', 'LICENSE', liquidMap)
await this.render('oocd.launch.liquid', 'oocd.launch', liquidMap)
await this.render('jlink.launch.liquid', 'jlink.launch', liquidMap)
await this.render('package.json.liquid', 'package.json', liquidMap)
await this.render('README.md.liquid', 'README.md', liquidMap)
await this.render('xmake.json.liquid', 'xmake.json', liquidMap)
// Exit upon first error.
shell.set('-e')
shell.mkdir('-p', 'include')
await this.render(`include/led-${fileExtension}.h.liquid`,
'include/led.h', liquidMap)
shell.cp(path.resolve(templatesPath, `include/sysclock-${fileExtension}.h`),
'include/sysclock.h')
log.info(`File 'include/sysclock.h' copied.`)
shell.mkdir('-p', 'ldscripts')
shell.cp(path.resolve(templatesPath, 'ldscripts/libs.ld'),
'ldscripts/libs.ld')
log.info(`File 'ldscripts/libs.ld' copied.`)
shell.cp(path.resolve(templatesPath, `ldscripts/mem-${boardName}.ld`),
'ldscripts/mem.ld')
log.info(`File 'ldscripts/mem.ld' copied.`)
shell.cp(path.resolve(templatesPath, 'ldscripts/sections.ld'),
'ldscripts/sections.ld')
log.info(`File 'ldscripts/sections.ld' copied.`)
shell.mkdir('-p', 'src')
await this.render('src/initialize-hardware.c.cpp.liquid',
`src/initialize-hardware.${fileExtension}`, liquidMap)
await this.render('src/interrupts-handlers.c.cpp.liquid',
`src/interrupts-handlers.${fileExtension}`, liquidMap)
shell.cp(path.resolve(templatesPath, `src/led.${fileExtension}`),
`src/led.${fileExtension}`)
log.info(`File 'src/led.${fileExtension}' copied.`)
await this.render('src/main.c.cpp.liquid',
`src/main.${fileExtension}`, liquidMap)
shell.cp(path.resolve(templatesPath, 'src/newlib-syscalls.c'),
'src/newlib-syscalls.c')
log.info(`File 'src/newlib-syscalls.c' copied.`)
shell.cp(path.resolve(templatesPath, `src/sysclock.${fileExtension}`),
`src/sysclock.${fileExtension}`)
log.info(`File 'src/sysclock.${fileExtension}' copied.`)
}
}
// ----------------------------------------------------------------------------
// Node.js specific export definitions.
// By default, `module.exports = {}`.
// The Template class is added as a property to this object.
module.exports.properties = properties
module.exports.XpmInitTemplate = XpmInitTemplate
// In ES6, it would be:
// export class XpmInitTemplate { ... }
// ...
// import { XpmInitTemplate } from 'template.js'
// ----------------------------------------------------------------------------