├── .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 |
--------------------------------------------------------------------------------