├── .prettierrc ├── .gitignore ├── tsconfig.json ├── src ├── util.ts ├── index.spec.ts └── index.ts ├── package.json ├── UNLICENSE └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ninja-builder-*.tgz 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "files": ["src/index.ts", "src/util.ts"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export interface Writable { 2 | write(s: string): boolean; 3 | } 4 | 5 | export class StringWritable implements Writable { 6 | private readonly chunks: string[] = []; 7 | 8 | write(s: string): boolean { 9 | this.chunks.push(s); 10 | return true; 11 | } 12 | 13 | toString(): string { 14 | return this.chunks.join(''); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ninja-builder", 3 | "version": "0.0.3", 4 | "description": "Create ninja build files programmatically in JS/TS", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "prepare": "tsc", 9 | "format": "prettier --write src/*.ts", 10 | "test": "mocha -r ts-node/register src/**.spec.ts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/zacharyvoase/ninja-builder.git" 15 | }, 16 | "keywords": [ 17 | "ninja", 18 | "build", 19 | "generator", 20 | "typescript" 21 | ], 22 | "author": "Zack Voase", 23 | "license": "Unlicense", 24 | "bugs": { 25 | "url": "https://github.com/zacharyvoase/ninja-builder/issues" 26 | }, 27 | "homepage": "https://github.com/zacharyvoase/ninja-builder#readme", 28 | "devDependencies": { 29 | "@tsconfig/node18": "^1.0.1", 30 | "@types/chai": "^4.3.1", 31 | "@types/mocha": "^9.1.1", 32 | "chai": "^4.3.6", 33 | "dedent-js": "^1.0.1", 34 | "mocha": "^10.0.0", 35 | "prettier": "^2.7.1", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.7.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ninja-builder: Build Ninja build files from JS/Typescript 2 | 3 | Does what it says on the packaging. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install [--save|--save-dev] ninja-builder 9 | ``` 10 | 11 | ## Example 12 | 13 | This JS code: 14 | 15 | ```typescript 16 | import { NinjaBuilder } from 'ninja-builder'; 17 | 18 | const ninja = new NinjaBuilder({ 19 | requiredVersion: '1.7', 20 | default: ['hello'], 21 | }); 22 | 23 | ninja.bind('cflags', '-Wall'); 24 | 25 | ninja.rule('cc', 'cc $in $cflags -o $out'); 26 | 27 | ninja.build('hello', 'cc', 'hello.c'); 28 | 29 | ninja.writeTo(process.stdout); 30 | ``` 31 | 32 | Produces the following ninja file: 33 | 34 | ```ninja 35 | ninja_required_version = 1.7 36 | cflags = -Wall 37 | rule cc 38 | command = cc $in $cflags $out 39 | build hello: cc hello.c 40 | default hello 41 | ``` 42 | 43 | There are more options you can provide to each of `NinjaBuilder()`, `rule()` 44 | and `build()`; check out the source code to see what's supported. 45 | 46 | ## Unlicense 47 | 48 | This is free and unencumbered software released into the public domain. 49 | 50 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 51 | software, either in source code form or as a compiled binary, for any purpose, 52 | commercial or non-commercial, and by any means. 53 | 54 | In jurisdictions that recognize copyright laws, the author or authors of this 55 | software dedicate any and all copyright interest in the software to the public 56 | domain. We make this dedication for the benefit of the public at large and to 57 | the detriment of our heirs and successors. We intend this dedication to be an 58 | overt act of relinquishment in perpetuity of all present and future rights to 59 | this software under copyright law. 60 | 61 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 62 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 63 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 64 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 65 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 66 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 67 | 68 | For more information, please refer to 69 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { NinjaBuildFile } from './index'; 2 | import { assert } from 'chai'; 3 | import dedent from 'dedent-js'; 4 | 5 | describe('NinjaBuild', () => { 6 | it('produces an empty file', () => { 7 | const ninja = new NinjaBuildFile(); 8 | check('', ninja); 9 | }); 10 | 11 | it('asserts a minimum version', () => { 12 | const ninja = new NinjaBuildFile({ requiredVersion: '1.7' }); 13 | check( 14 | ` 15 | ninja_required_version = 1.7 16 | `, 17 | ninja 18 | ); 19 | }); 20 | 21 | it('sets a builddir', () => { 22 | const ninja = new NinjaBuildFile({ 23 | requiredVersion: '1.7', 24 | builddir: 'dist', 25 | }); 26 | check( 27 | ` 28 | ninja_required_version = 1.7 29 | builddir = dist 30 | `, 31 | ninja 32 | ); 33 | }); 34 | 35 | it('sets one default output', () => { 36 | const ninja = new NinjaBuildFile({ 37 | requiredVersion: '1.7', 38 | builddir: 'dist', 39 | default: 'a.out', 40 | }); 41 | check( 42 | ` 43 | ninja_required_version = 1.7 44 | builddir = dist 45 | default a.out 46 | `, 47 | ninja 48 | ); 49 | }); 50 | 51 | it('sets multiple default rules', () => { 52 | const ninja = new NinjaBuildFile({ 53 | requiredVersion: '1.7', 54 | builddir: 'dist', 55 | default: ['foo', 'bar'], 56 | }); 57 | check( 58 | ` 59 | ninja_required_version = 1.7 60 | builddir = dist 61 | default foo bar 62 | `, 63 | ninja 64 | ); 65 | }); 66 | 67 | it('defines a binding', () => { 68 | const ninja = new NinjaBuildFile({ 69 | requiredVersion: '1.7', 70 | builddir: 'dist', 71 | default: ['foo', 'bar'], 72 | }); 73 | ninja.bind('cflags', '-Wall'); 74 | check( 75 | ` 76 | ninja_required_version = 1.7 77 | builddir = dist 78 | cflags = -Wall 79 | default foo bar 80 | `, 81 | ninja 82 | ); 83 | }); 84 | 85 | it('defines a rule', () => { 86 | const ninja = new NinjaBuildFile({ 87 | requiredVersion: '1.7', 88 | builddir: 'dist', 89 | default: ['foo', 'bar'], 90 | }); 91 | ninja.rule('cc', 'cc $in -o $out'); 92 | check( 93 | ` 94 | ninja_required_version = 1.7 95 | builddir = dist 96 | rule cc 97 | command = cc $in -o $out 98 | default foo bar 99 | `, 100 | ninja 101 | ); 102 | }); 103 | 104 | it('defines a build edge', () => { 105 | const ninja = new NinjaBuildFile({ 106 | requiredVersion: '1.7', 107 | builddir: 'dist', 108 | default: ['foo', 'bar'], 109 | }); 110 | ninja.build('foo', 'cc', 'bar'); 111 | check( 112 | ` 113 | ninja_required_version = 1.7 114 | builddir = dist 115 | build foo: cc bar 116 | default foo bar 117 | `, 118 | ninja 119 | ); 120 | }); 121 | 122 | it('adds raw strings/comments/etc. to the output', () => { 123 | const ninja = new NinjaBuildFile({ 124 | requiredVersion: '1.7', 125 | default: 'hello', 126 | }); 127 | ninja.bind('cflags', '-Wall'); 128 | ninja.raw('# This is a poor implementation of a cc rule.'); 129 | ninja.rule('cc', 'cc $cflags $in -o $out'); 130 | ninja.raw('# This is also not that great.'); 131 | ninja.build('hello', 'cc', 'hello.c'); 132 | check( 133 | ` 134 | ninja_required_version = 1.7 135 | cflags = -Wall 136 | # This is a poor implementation of a cc rule. 137 | rule cc 138 | command = cc $cflags $in -o $out 139 | # This is also not that great. 140 | build hello: cc hello.c 141 | default hello 142 | `, 143 | ninja 144 | ); 145 | }); 146 | 147 | it('adds pool declarations', () => { 148 | const ninja = new NinjaBuildFile({ 149 | requiredVersion: '1.7', 150 | }); 151 | ninja.pool('link_pool', 4); 152 | check( 153 | ` 154 | ninja_required_version = 1.7 155 | pool link_pool 156 | depth = 4 157 | `, 158 | ninja 159 | ); 160 | }); 161 | 162 | it('adds includes', () => { 163 | const ninja = new NinjaBuildFile({ 164 | requiredVersion: '1.7', 165 | }); 166 | ninja.include('another.ninja'); 167 | check( 168 | ` 169 | ninja_required_version = 1.7 170 | include another.ninja 171 | `, 172 | ninja 173 | ); 174 | }); 175 | 176 | it('adds subninjas', () => { 177 | const ninja = new NinjaBuildFile({ 178 | requiredVersion: '1.7', 179 | }); 180 | ninja.subninja('another.ninja'); 181 | check( 182 | ` 183 | ninja_required_version = 1.7 184 | subninja another.ninja 185 | `, 186 | ninja 187 | ); 188 | }); 189 | }); 190 | 191 | function check(expected: string, ninja: { toString(): string }) { 192 | assert.strictEqual(ninja.toString().trimEnd(), dedent(expected).trimEnd()); 193 | } 194 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Writable, StringWritable } from './util'; 2 | 3 | // The indent to use for rule/edge options etc. 4 | const INDENT = ' '; 5 | 6 | /** Any kind of node that can be output to a Ninja file. */ 7 | abstract class NinjaNode { 8 | /** Write this node to something with a write(s: string) method. */ 9 | abstract writeTo(w: Writable): void; 10 | 11 | /** Convert this node to a string, using its writeTo() method. */ 12 | toString(): string { 13 | const writable = new StringWritable(); 14 | this.writeTo(writable); 15 | return writable.toString(); 16 | } 17 | } 18 | 19 | export type BuildFileOptions = { 20 | requiredVersion?: string; 21 | builddir?: string; 22 | default?: string | string[]; 23 | }; 24 | 25 | export type RuleOptions = { 26 | depfile?: string; 27 | deps?: 'gcc' | 'msvc'; 28 | msvc_deps_prefix?: string; 29 | description?: string; 30 | dyndep?: string; 31 | generator?: boolean; 32 | in?: string; 33 | in_newline?: string; 34 | out?: string; 35 | restat?: boolean; 36 | } & ({} | { rspfile: string; rspfile_content: string }); 37 | 38 | export type EdgeOptions = { 39 | implicitOuts?: string[]; 40 | implicitDeps?: string[]; 41 | orderDeps?: string[]; 42 | validations?: string[]; 43 | dyndep?: string; 44 | pool?: string; 45 | }; 46 | 47 | export class NinjaBuildFile extends NinjaNode { 48 | private readonly nodes: NinjaNode[] = []; 49 | 50 | constructor(public readonly options?: BuildFileOptions) { 51 | super(); 52 | } 53 | 54 | private preamble(): NinjaNode[] { 55 | const preambleNodes = []; 56 | if (this.options?.requiredVersion != null) { 57 | preambleNodes.push( 58 | new Binding('ninja_required_version', this.options.requiredVersion) 59 | ); 60 | } 61 | if (this.options?.builddir != null) { 62 | preambleNodes.push(new Binding('builddir', this.options.builddir)); 63 | } 64 | return preambleNodes; 65 | } 66 | 67 | private postamble(): NinjaNode[] { 68 | const postambleNodes = []; 69 | if (this.options?.default != null) { 70 | postambleNodes.push(new Raw(`default ${fileList(this.options.default)}`)); 71 | } 72 | return postambleNodes; 73 | } 74 | 75 | override writeTo(w: Writable): void { 76 | for (const node of [ 77 | ...this.preamble(), 78 | ...this.nodes, 79 | ...this.postamble(), 80 | ]) { 81 | node.writeTo(w); 82 | } 83 | } 84 | 85 | /** Bind a variable name to a value. */ 86 | bind(name: string, value: string): this { 87 | this.nodes.push(new Binding(name, value)); 88 | return this; 89 | } 90 | 91 | /** Define a named rule. */ 92 | rule(name: string, command: string, options?: RuleOptions): this { 93 | this.nodes.push(new Rule(name, command, options)); 94 | return this; 95 | } 96 | 97 | /** Define a build edge (how to build outputs from inputs via a rule). */ 98 | build( 99 | outputs: string | string[], 100 | rule: string, 101 | inputs: string | string[], 102 | options?: EdgeOptions, 103 | bindings?: Record 104 | ): this { 105 | this.nodes.push(new Edge(outputs, rule, inputs, options, bindings)); 106 | return this; 107 | } 108 | 109 | /** Add a raw string to the output. A newline will be added to the end. */ 110 | raw(s: string): this { 111 | this.nodes.push(new Raw(s)); 112 | return this; 113 | } 114 | 115 | /** Add a subninja reference. */ 116 | subninja(path: string): this { 117 | this.nodes.push(new Raw(`subninja ${path}`)); 118 | return this; 119 | } 120 | 121 | /** Add an include reference. */ 122 | include(path: string): this { 123 | this.nodes.push(new Raw(`include ${path}`)); 124 | return this; 125 | } 126 | 127 | /** Define a pool with a given depth. */ 128 | pool(name: string, depth: number): this { 129 | this.nodes.push(new Raw(`pool ${name}\n${INDENT}depth = ${depth}`)); 130 | return this; 131 | } 132 | } 133 | 134 | class Raw extends NinjaNode { 135 | constructor(public readonly string: string) { 136 | super(); 137 | } 138 | 139 | override writeTo(w: Writable): void { 140 | w.write(this.string + '\n'); 141 | } 142 | } 143 | 144 | class Binding extends NinjaNode { 145 | constructor( 146 | public readonly name: string, 147 | public readonly value: string, 148 | public readonly indent: string = '' 149 | ) { 150 | super(); 151 | } 152 | 153 | override writeTo(w: Writable): void { 154 | w.write(`${this.indent}${this.name} = ${this.value}\n`); 155 | } 156 | } 157 | 158 | class Rule extends NinjaNode { 159 | constructor( 160 | public readonly name: string, 161 | public readonly command: string, 162 | public readonly options?: RuleOptions 163 | ) { 164 | super(); 165 | } 166 | 167 | override writeTo(w: Writable): void { 168 | w.write(`rule ${this.name}\n`); 169 | new Binding('command', this.command, INDENT).writeTo(w); 170 | for (const [opt, value] of Object.entries(this.options ?? {})) { 171 | if (typeof value === 'boolean') { 172 | if (value) { 173 | new Binding(opt, '1', INDENT).writeTo(w); 174 | } 175 | } else { 176 | new Binding(opt, value, INDENT).writeTo(w); 177 | } 178 | } 179 | } 180 | } 181 | 182 | class Edge extends NinjaNode { 183 | constructor( 184 | public readonly outputs: string | string[], 185 | public readonly rule: string, 186 | public readonly inputs?: string | string[], 187 | public readonly options?: EdgeOptions, 188 | public readonly bindings?: Record 189 | ) { 190 | super(); 191 | } 192 | 193 | override writeTo(w: Writable): void { 194 | const outputs = [ 195 | fileList(this.outputs), 196 | fileList(this.options?.implicitOuts, '|'), 197 | ].join(''); 198 | 199 | const inputs = [ 200 | fileList(this.inputs), 201 | fileList(this.options?.implicitDeps, '|'), 202 | fileList(this.options?.orderDeps, '||'), 203 | fileList(this.options?.validations, '|@'), 204 | ].join(''); 205 | 206 | w.write(`build ${outputs}: ${this.rule} ${inputs}\n`); 207 | if (this.options?.dyndep) { 208 | new Binding('dyndep', this.options.dyndep, INDENT).writeTo(w); 209 | } 210 | if (this.options?.pool) { 211 | new Binding('pool', this.options.pool, INDENT).writeTo(w); 212 | } 213 | for (const [name, value] of Object.entries(this.bindings ?? {})) { 214 | new Binding(name, value, INDENT).writeTo(w); 215 | } 216 | } 217 | } 218 | 219 | /** Helper for building lists of filenames for build edges. */ 220 | function fileList(f?: string | string[], separator?: string) { 221 | if (f == null || f.length === 0) { 222 | return ''; 223 | } else if (typeof f === 'string') { 224 | f = [f]; 225 | } 226 | const joined = f.join(' '); 227 | if (separator != null && separator !== '') { 228 | return ` ${separator} ${joined}`; 229 | } 230 | return joined; 231 | } 232 | 233 | /** Escape a string literal for use in a Ninja file. */ 234 | export function escape(s: string): string { 235 | return s.replace(/[ \:\$\n]/g, (match) => '$' + match); 236 | } 237 | --------------------------------------------------------------------------------