amxx-builder 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +111 -0
- package/README.md +485 -0
- package/action-entry.js +42 -0
- package/action.yml +55 -0
- package/defaults/amxbuild.defaults.yml +40 -0
- package/index.js +4 -0
- package/mcp/dep-resolver.js +75 -0
- package/mcp/handlers.js +862 -0
- package/mcp/mcp-server.js +112 -0
- package/mcp/registry.js +863 -0
- package/mcp/symbol-index.js +171 -0
- package/package.json +68 -0
- package/src/archiver.js +140 -0
- package/src/asset-fetcher.js +277 -0
- package/src/build-plan.js +101 -0
- package/src/build-service.js +188 -0
- package/src/cache-dir.js +21 -0
- package/src/cache-info.js +104 -0
- package/src/cli.js +302 -0
- package/src/collector.js +89 -0
- package/src/commands/build.js +49 -0
- package/src/commands/cache.js +77 -0
- package/src/commands/clean.js +33 -0
- package/src/commands/compile-renderer.js +38 -0
- package/src/commands/deploy.js +40 -0
- package/src/commands/deps-tree.js +92 -0
- package/src/commands/doctor.js +77 -0
- package/src/commands/dry-run.js +64 -0
- package/src/commands/init.js +228 -0
- package/src/commands/mcp.js +13 -0
- package/src/commands/releases.js +45 -0
- package/src/commands/resolve-manifest.js +27 -0
- package/src/commands/serve.js +489 -0
- package/src/commands/shared.js +24 -0
- package/src/commands/validate.js +34 -0
- package/src/commands/watch.js +209 -0
- package/src/compile-utils.js +65 -0
- package/src/compiler-fetcher.js +327 -0
- package/src/compiler.js +228 -0
- package/src/dep-graph.js +92 -0
- package/src/deployer.js +197 -0
- package/src/deps-resolver.js +127 -0
- package/src/deps-tree.js +202 -0
- package/src/env.js +18 -0
- package/src/events.js +29 -0
- package/src/format.js +23 -0
- package/src/fs-utils.js +87 -0
- package/src/include-tree.js +845 -0
- package/src/ini-builder.js +44 -0
- package/src/jsonrpc-transport.js +195 -0
- package/src/logger.js +50 -0
- package/src/manifest-path.js +34 -0
- package/src/manifest.js +373 -0
- package/src/progress.js +66 -0
- package/src/rcon.js +103 -0
- package/src/release-fetcher.js +206 -0
- package/src/release-lister.js +79 -0
- package/src/repo-fetcher.js +273 -0
- package/src/retry.js +50 -0
- package/src/schema.js +54 -0
- package/src/update-check.js +114 -0
- package/src/validate.js +69 -0
- package/src/watcher.js +135 -0
- package/templates/init-build.bat +11 -0
- package/templates/init-build.sh +7 -0
- package/templates/init-deploy.env +14 -0
- package/templates/init-manifest.yml +6 -0
- package/templates/init-workflow.yml +59 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AMXX Include Tree — bidirectional #include dependency graph builder.
|
|
5
|
+
*
|
|
6
|
+
* Core engine that:
|
|
7
|
+
* - Scans all .sma files (local + repos) as roots
|
|
8
|
+
* - Parses #include / #tryinclude directives in .sma and .inc files
|
|
9
|
+
* - Resolves includes using the same logic as amxxpc (quoted → local first,
|
|
10
|
+
* angle → include dirs)
|
|
11
|
+
* - Detects include guards (#if defined … #endinput … #endif … #define)
|
|
12
|
+
* to avoid re-expanding already-guarded files within a root's tree
|
|
13
|
+
* - Builds a bidirectional graph so you can traverse both DOWN
|
|
14
|
+
* (root → transitive includes) and UP (leaf → roots that reach it)
|
|
15
|
+
* - Formats the result as a human-readable tree or raw JSON
|
|
16
|
+
*
|
|
17
|
+
* Public API:
|
|
18
|
+
* const { buildIncludeTree } = require('./include-tree');
|
|
19
|
+
* const result = await buildIncludeTree(manifestPath, targetPath, options);
|
|
20
|
+
* // result.text — formatted tree string
|
|
21
|
+
* // result.tree — structured tree data (JSON-safe)
|
|
22
|
+
* // result.graph — full graph object (for programmatic use)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const glob = require('fast-glob');
|
|
28
|
+
|
|
29
|
+
const { parseManifest, parseDepsLines, resolveGithubToken } = require('./manifest');
|
|
30
|
+
const { fetchCompiler, fetchLatestVersion } = require('./compiler-fetcher');
|
|
31
|
+
const { fetchRepo, resolveRefIfLatest, resolveRepoRefs } = require('./repo-fetcher');
|
|
32
|
+
const { fetchReleaseDep } = require('./release-fetcher');
|
|
33
|
+
const { normalize } = require('./deps-resolver');
|
|
34
|
+
const { loadEnv } = require('./env');
|
|
35
|
+
const { resolveManifestPath } = require('./manifest-path');
|
|
36
|
+
|
|
37
|
+
// ─── Regex ───────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Matches #include <file> and #include "file" (and #tryinclude variants). */
|
|
40
|
+
const RE_INCLUDE = /^[ \t]*#(?:try)?include[ \t]+([<"])([^>"]+)[>"][ \t]*(?:\/\/.*)?$/gm;
|
|
41
|
+
|
|
42
|
+
/** Matches #if defined <GUARD> — the start of an include guard block. */
|
|
43
|
+
const RE_IF_DEFINED = /^[ \t]*#if[ \t]+defined[ \t]+(\w+)/m;
|
|
44
|
+
|
|
45
|
+
/** Matches #define <GUARD> — the companion define of an include guard. */
|
|
46
|
+
const RE_DEFINE = (guard) => new RegExp(
|
|
47
|
+
'^[ \\t]*#define[ \\t]+' + guard.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
48
|
+
'm'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// ─── IncludeGraph — bidirectional graph of #include relationships ────────────
|
|
52
|
+
|
|
53
|
+
class IncludeGraph {
|
|
54
|
+
constructor() {
|
|
55
|
+
/** Map<absPath, IncludeNode> */
|
|
56
|
+
this.nodes = new Map();
|
|
57
|
+
|
|
58
|
+
/** absPath of all discovered .sma root files */
|
|
59
|
+
this.roots = [];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ordered list of directories and their labels for resolving
|
|
63
|
+
* <angle> includes (and "quoted" fallback).
|
|
64
|
+
* @type {{ path: string, label: string }[]}
|
|
65
|
+
*/
|
|
66
|
+
this.includeDirs = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Node management ──────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get or create a node for an absolute path.
|
|
73
|
+
* @param {string} absPath
|
|
74
|
+
* @param {object} [meta] - Optional metadata (name, source, type, guard, error)
|
|
75
|
+
* @returns {IncludeNode}
|
|
76
|
+
*/
|
|
77
|
+
node(absPath, meta) {
|
|
78
|
+
let n = this.nodes.get(absPath);
|
|
79
|
+
if (!n) {
|
|
80
|
+
n = new IncludeNode(absPath, meta);
|
|
81
|
+
this.nodes.set(absPath, n);
|
|
82
|
+
}
|
|
83
|
+
if (meta) {
|
|
84
|
+
if (meta.name != null) n.name = meta.name;
|
|
85
|
+
if (meta.source != null) n.source = meta.source;
|
|
86
|
+
if (meta.type != null) n.type = meta.type;
|
|
87
|
+
if (meta.guard != null) n.guard = meta.guard;
|
|
88
|
+
if (meta.error != null) n.error = meta.error;
|
|
89
|
+
}
|
|
90
|
+
return n;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Add a directed edge: fromFile includes toFile.
|
|
95
|
+
* Also stores how the include was spelled (<name> or "name").
|
|
96
|
+
*/
|
|
97
|
+
addEdge(fromPath, toPath, { name, isAngle } = {}) {
|
|
98
|
+
const from = this.node(fromPath);
|
|
99
|
+
const to = this.node(toPath);
|
|
100
|
+
const display = isAngle ? `<${name}>` : `"${name}"`;
|
|
101
|
+
// Avoid duplicates
|
|
102
|
+
if (!from.includes.some(e => e.absPath === toPath)) {
|
|
103
|
+
from.includes.push({ absPath: toPath, display });
|
|
104
|
+
to.includedBy.push({ absPath: fromPath, display });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Include guard detection ──────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect an include guard in a file's content.
|
|
112
|
+
* Returns the guard symbol or null.
|
|
113
|
+
*
|
|
114
|
+
* Pattern:
|
|
115
|
+
* #if defined <GUARD>
|
|
116
|
+
* #endinput
|
|
117
|
+
* #endif
|
|
118
|
+
* #define <GUARD>
|
|
119
|
+
*/
|
|
120
|
+
static detectGuard(content) {
|
|
121
|
+
const m = content.match(RE_IF_DEFINED);
|
|
122
|
+
if (!m) return null;
|
|
123
|
+
const guard = m[1];
|
|
124
|
+
// #endinput must be present — signals "stop reading here"
|
|
125
|
+
if (!content.includes('#endinput')) return null;
|
|
126
|
+
// #define <GUARD> must exist somewhere after the #if block
|
|
127
|
+
if (!RE_DEFINE(guard).test(content)) return null;
|
|
128
|
+
return guard;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Include parsing ──────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract #include/#tryinclude directives from file content.
|
|
135
|
+
* @param {string} content
|
|
136
|
+
* @returns {{ name: string, isAngle: boolean }[]}
|
|
137
|
+
*/
|
|
138
|
+
static parseIncludesFromContent(content) {
|
|
139
|
+
const result = [];
|
|
140
|
+
|
|
141
|
+
// Use a fresh regex instance per call to avoid state leakage
|
|
142
|
+
const re = new RegExp(RE_INCLUDE.source, 'gm');
|
|
143
|
+
let m;
|
|
144
|
+
while ((m = re.exec(content)) !== null) {
|
|
145
|
+
result.push({ name: m[2].trim(), isAngle: m[1] === '<' });
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read and parse includes from a file on disk.
|
|
152
|
+
* @param {string} absPath
|
|
153
|
+
* @returns {{ name: string, isAngle: boolean }[]}
|
|
154
|
+
*/
|
|
155
|
+
static readIncludes(absPath) {
|
|
156
|
+
let content;
|
|
157
|
+
try {
|
|
158
|
+
content = fs.readFileSync(absPath, 'utf8');
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return IncludeGraph.parseIncludesFromContent(content);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Include resolution ───────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolve an #include name to an absolute file path.
|
|
169
|
+
*
|
|
170
|
+
* Resolution order mirrors amxxpc:
|
|
171
|
+
* 1. "quoted" includes — check the including file's own directory first
|
|
172
|
+
* 2. <angle> or fallback — search includeDirs in order
|
|
173
|
+
* 3. Case-insensitive fallback on all of the above
|
|
174
|
+
*
|
|
175
|
+
* @param {string} fromFile - Absolute path of the file doing the #include
|
|
176
|
+
* @param {string} name - The bare filename from the directive (<X> or "X")
|
|
177
|
+
* @param {boolean} isAngle - Whether the include was <angle> style
|
|
178
|
+
* @returns {{ absPath: string, source: string|null } | null}
|
|
179
|
+
*/
|
|
180
|
+
resolveInclude(fromFile, name, isAngle) {
|
|
181
|
+
const withExt = /\.inc$/i.test(name) ? name : name + '.inc';
|
|
182
|
+
|
|
183
|
+
// ── 1. "quoted" — check the including file's own directory ──
|
|
184
|
+
if (!isAngle) {
|
|
185
|
+
const rel = path.resolve(path.dirname(fromFile), withExt);
|
|
186
|
+
if (fs.existsSync(rel)) {
|
|
187
|
+
return { absPath: rel, source: null };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── 2. Search include dirs (exact match first) ──
|
|
192
|
+
for (const dir of this.includeDirs) {
|
|
193
|
+
const full = path.join(dir.path, withExt);
|
|
194
|
+
if (fs.existsSync(full)) {
|
|
195
|
+
return { absPath: full, source: dir.label };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── 3. "quoted" — case-insensitive fallback in file's own dir ──
|
|
200
|
+
if (!isAngle) {
|
|
201
|
+
const ci = findCaseInsensitive(path.dirname(fromFile), withExt);
|
|
202
|
+
if (ci) return { absPath: ci, source: null };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── 4. Case-insensitive fallback in include dirs ──
|
|
206
|
+
for (const dir of this.includeDirs) {
|
|
207
|
+
const ci = findCaseInsensitive(dir.path, withExt);
|
|
208
|
+
if (ci) return { absPath: ci, source: dir.label };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null; // unresolvable
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Graph building ───────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Recursively parse a file and all its #include'd files.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} absPath - Absolute path to parse
|
|
220
|
+
* @param {Set<string>} guards - Active include guards on the current path
|
|
221
|
+
* @param {Set<string>} [stack] - Recursion stack for cycle detection (per-root)
|
|
222
|
+
* @param {number} depth - Safety limit
|
|
223
|
+
*/
|
|
224
|
+
parseFile(absPath, guards = new Set(), stack = new Set(), depth = 0) {
|
|
225
|
+
if (depth > 500) return;
|
|
226
|
+
if (!fs.existsSync(absPath)) return;
|
|
227
|
+
|
|
228
|
+
const n = this.node(absPath);
|
|
229
|
+
|
|
230
|
+
// Read content and detect guard if not already known
|
|
231
|
+
let content;
|
|
232
|
+
try {
|
|
233
|
+
content = fs.readFileSync(absPath, 'utf8');
|
|
234
|
+
} catch {
|
|
235
|
+
n.error = 'unreadable';
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!n.guard) {
|
|
240
|
+
n.guard = IncludeGraph.detectGuard(content);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Guard already set on this path → don't re-expand
|
|
244
|
+
if (n.guard && guards.has(n.guard)) return;
|
|
245
|
+
|
|
246
|
+
// File already in current recursion stack → cycle without guards.
|
|
247
|
+
// The edge is already recorded (addEdge below), just don't expand
|
|
248
|
+
// children to avoid infinite recursion.
|
|
249
|
+
if (stack.has(absPath)) return;
|
|
250
|
+
|
|
251
|
+
if (n.guard) guards.add(n.guard);
|
|
252
|
+
stack.add(absPath);
|
|
253
|
+
|
|
254
|
+
// Parse and recurse into includes
|
|
255
|
+
const includes = IncludeGraph.parseIncludesFromContent(content);
|
|
256
|
+
for (const inc of includes) {
|
|
257
|
+
const resolved = this.resolveInclude(absPath, inc.name, inc.isAngle);
|
|
258
|
+
if (resolved) {
|
|
259
|
+
// Ensure target node exists (with a fallback name)
|
|
260
|
+
this.node(resolved.absPath, {
|
|
261
|
+
name: path.basename(resolved.absPath),
|
|
262
|
+
source: resolved.source || n.source,
|
|
263
|
+
});
|
|
264
|
+
this.addEdge(absPath, resolved.absPath, inc);
|
|
265
|
+
// Siblings share guards: a sibling that sets guard G must be visible
|
|
266
|
+
// to the next sibling (same compile unit). Do NOT clone `guards`.
|
|
267
|
+
this.parseFile(resolved.absPath, guards, stack, depth + 1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
stack.delete(absPath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build the full graph by scanning all root .sma files.
|
|
276
|
+
* Must be called after roots and includeDirs are populated.
|
|
277
|
+
*/
|
|
278
|
+
build() {
|
|
279
|
+
for (const root of this.roots) {
|
|
280
|
+
this.parseFile(root, new Set(), new Set());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Tree traversal ───────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Walk DOWN from a node: file → its #includes → their #includes, etc.
|
|
288
|
+
* Tracks include guards per path so guarded files are not re-expanded.
|
|
289
|
+
* Uses recursion-stack cycle detection so circular includes without
|
|
290
|
+
* guards don't loop infinitely.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} absPath
|
|
293
|
+
* @param {Set<string>} [guards]
|
|
294
|
+
* @param {Set<string>} [stack] - Recursion stack for cycle detection
|
|
295
|
+
* @returns {object|null} Tree node data
|
|
296
|
+
*/
|
|
297
|
+
walkDown(absPath, guards = new Set(), stack = new Set()) {
|
|
298
|
+
const n = this.nodes.get(absPath);
|
|
299
|
+
if (!n) return null;
|
|
300
|
+
|
|
301
|
+
const isGuarded = !!(n.guard && guards.has(n.guard));
|
|
302
|
+
const inCycle = stack.has(absPath);
|
|
303
|
+
|
|
304
|
+
const tree = {
|
|
305
|
+
name: n.name,
|
|
306
|
+
absPath,
|
|
307
|
+
source: n.source,
|
|
308
|
+
guard: n.guard,
|
|
309
|
+
isGuarded,
|
|
310
|
+
cycle: inCycle && !isGuarded, // cycle detected (without guard)
|
|
311
|
+
error: n.error,
|
|
312
|
+
children: [],
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (isGuarded || n.error || inCycle) return tree;
|
|
316
|
+
|
|
317
|
+
if (n.guard) guards.add(n.guard);
|
|
318
|
+
stack.add(absPath);
|
|
319
|
+
|
|
320
|
+
for (const edge of n.includes) {
|
|
321
|
+
// Siblings share guards (same compile unit). Do NOT clone.
|
|
322
|
+
const child = this.walkDown(edge.absPath, guards, stack);
|
|
323
|
+
if (child) {
|
|
324
|
+
child._display = edge.display;
|
|
325
|
+
tree.children.push(child);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
stack.delete(absPath);
|
|
330
|
+
return tree;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Walk UP from a node: file → files that #include it → their includers, etc.
|
|
335
|
+
* Uses per-path cycle detection to prevent infinite loops.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} absPath
|
|
338
|
+
* @param {Set<string>} [pathVisited]
|
|
339
|
+
* @returns {object|null} Tree node data
|
|
340
|
+
*/
|
|
341
|
+
walkUp(absPath, pathVisited = new Set()) {
|
|
342
|
+
if (pathVisited.has(absPath)) return null;
|
|
343
|
+
pathVisited.add(absPath);
|
|
344
|
+
|
|
345
|
+
const n = this.nodes.get(absPath);
|
|
346
|
+
if (!n) return null;
|
|
347
|
+
|
|
348
|
+
const tree = {
|
|
349
|
+
name: n.name,
|
|
350
|
+
absPath,
|
|
351
|
+
source: n.source,
|
|
352
|
+
guard: n.guard,
|
|
353
|
+
isGuarded: false,
|
|
354
|
+
error: n.error,
|
|
355
|
+
includedBy: [],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
for (const edge of n.includedBy) {
|
|
359
|
+
const parent = this.walkUp(edge.absPath, new Set(pathVisited));
|
|
360
|
+
if (parent) {
|
|
361
|
+
parent._display = edge.display; // how this file was included by parent
|
|
362
|
+
tree.includedBy.push(parent);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return tree;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Resolve a target path to a node in the graph.
|
|
371
|
+
* If the path is not already in the graph, tries to add it as a standalone node.
|
|
372
|
+
*
|
|
373
|
+
* @param {string} targetPath
|
|
374
|
+
* @returns {string} Resolved absolute path
|
|
375
|
+
*/
|
|
376
|
+
resolveTarget(targetPath) {
|
|
377
|
+
const abs = path.resolve(targetPath);
|
|
378
|
+
if (this.nodes.has(abs)) return abs;
|
|
379
|
+
|
|
380
|
+
// Try to add the file if it exists
|
|
381
|
+
if (fs.existsSync(abs)) {
|
|
382
|
+
const name = path.basename(abs);
|
|
383
|
+
const type = abs.endsWith('.sma') ? 'sma' : 'inc';
|
|
384
|
+
this.node(abs, { name, type, source: null });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return abs;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── IncludeNode ─────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
class IncludeNode {
|
|
394
|
+
constructor(absPath, meta = {}) {
|
|
395
|
+
this.absPath = absPath;
|
|
396
|
+
this.name = meta.name || path.basename(absPath);
|
|
397
|
+
this.source = meta.source || null;
|
|
398
|
+
this.type = meta.type || (absPath.endsWith('.sma') ? 'sma' : 'inc');
|
|
399
|
+
this.guard = meta.guard || null;
|
|
400
|
+
this.error = meta.error || null;
|
|
401
|
+
|
|
402
|
+
/** @type {{ absPath: string, display: string }[]} */
|
|
403
|
+
this.includes = [];
|
|
404
|
+
|
|
405
|
+
/** @type {{ absPath: string, display: string }[]} */
|
|
406
|
+
this.includedBy = [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Case-insensitive file search inside a directory, walking nested path segments
|
|
414
|
+
* (readdirSync only lists one level, so "SubDir/File.inc" needs per-segment lookup).
|
|
415
|
+
* Returns the first match (by readdir order) or null.
|
|
416
|
+
*/
|
|
417
|
+
function findCaseInsensitive(dir, filename) {
|
|
418
|
+
try {
|
|
419
|
+
const segments = filename.split(/[\\/]/);
|
|
420
|
+
let current = dir;
|
|
421
|
+
for (let i = 0; i < segments.length; i++) {
|
|
422
|
+
const lower = segments[i].toLowerCase();
|
|
423
|
+
if (i === segments.length - 1) {
|
|
424
|
+
for (const entry of fs.readdirSync(current)) {
|
|
425
|
+
if (entry.toLowerCase() === lower) return path.join(current, entry);
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
let found = null;
|
|
430
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
431
|
+
if (entry.isDirectory() && entry.name.toLowerCase() === lower) {
|
|
432
|
+
found = entry.name;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!found) return null;
|
|
437
|
+
current = path.join(current, found);
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
} catch (_) { return null; }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Depth-limit check: if depth is positive and current >= depth, stop.
|
|
445
|
+
*/
|
|
446
|
+
function reachedDepth(depth, current) {
|
|
447
|
+
return depth > 0 && current >= depth;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Format a tree node (from walkDown/walkUp) as text using Unicode box-drawing.
|
|
452
|
+
*
|
|
453
|
+
* @param {object} tree - Tree data from walkDown or walkUp
|
|
454
|
+
* @param {'down'|'up'} direction
|
|
455
|
+
* @param {number} [maxDepth]
|
|
456
|
+
* @returns {string}
|
|
457
|
+
*/
|
|
458
|
+
function renderTreeText(tree, direction, maxDepth) {
|
|
459
|
+
const lines = [];
|
|
460
|
+
|
|
461
|
+
const header = direction === 'up'
|
|
462
|
+
? `Include tree (UP) for ${tree.name}`
|
|
463
|
+
: `Include tree (DOWN) for ${tree.name}`;
|
|
464
|
+
|
|
465
|
+
lines.push('=== ' + header + ' ===');
|
|
466
|
+
lines.push('');
|
|
467
|
+
|
|
468
|
+
// Root line
|
|
469
|
+
const rootInfo = formatNodeInfo(tree, '');
|
|
470
|
+
lines.push(tree.name + rootInfo);
|
|
471
|
+
|
|
472
|
+
const children = direction === 'up' ? tree.includedBy : tree.children;
|
|
473
|
+
|
|
474
|
+
if (children && children.length > 0) {
|
|
475
|
+
for (let i = 0; i < children.length; i++) {
|
|
476
|
+
renderSubtree(children[i], '', i === children.length - 1, direction, maxDepth, 1, lines);
|
|
477
|
+
}
|
|
478
|
+
} else if (direction === 'up') {
|
|
479
|
+
lines.push(' (no .sma files include this file)');
|
|
480
|
+
} else {
|
|
481
|
+
lines.push(' (no #include directives)');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Legend
|
|
485
|
+
lines.push('');
|
|
486
|
+
lines.push('Legend:');
|
|
487
|
+
lines.push(' <file> — angle include (#include <file>)');
|
|
488
|
+
lines.push(' "file" — local include (#include "file")');
|
|
489
|
+
lines.push(' [guard] — include guard detected, expansion skipped');
|
|
490
|
+
|
|
491
|
+
return lines.join('\n');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function renderSubtree(node, prefix, isLast, direction, maxDepth, depth, lines) {
|
|
495
|
+
if (reachedDepth(maxDepth, depth)) {
|
|
496
|
+
lines.push(prefix + (isLast ? '└── ' : '├── ') + '… (max depth)');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
501
|
+
|
|
502
|
+
// Display name: use _display if available (the spelling from the parent)
|
|
503
|
+
let displayName = node._display || node.name;
|
|
504
|
+
const info = formatNodeInfo(node, displayName);
|
|
505
|
+
|
|
506
|
+
lines.push(prefix + connector + displayName + info);
|
|
507
|
+
|
|
508
|
+
const children = direction === 'up' ? node.includedBy : node.children;
|
|
509
|
+
if (children && children.length > 0) {
|
|
510
|
+
const childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
511
|
+
for (let i = 0; i < children.length; i++) {
|
|
512
|
+
renderSubtree(children[i], childPrefix, i === children.length - 1, direction, maxDepth, depth + 1, lines);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Format node metadata suffix (source, guard/error/not-found annotation).
|
|
519
|
+
*/
|
|
520
|
+
function formatNodeInfo(node, displayName) {
|
|
521
|
+
const parts = [];
|
|
522
|
+
|
|
523
|
+
if (node.source) {
|
|
524
|
+
parts.push(node.source);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (node.cycle) {
|
|
528
|
+
parts.push('cycle');
|
|
529
|
+
} else if (node.isGuarded) {
|
|
530
|
+
parts.push('guard already set');
|
|
531
|
+
} else if (node.guard) {
|
|
532
|
+
parts.push('guard: ' + node.guard);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (node.error) {
|
|
536
|
+
parts.push('ERROR: ' + node.error);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (parts.length === 0) return '';
|
|
540
|
+
return ' [' + parts.join(', ') + ']';
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Build a directed graph of #include relationships for an AMXX project and
|
|
547
|
+
* produce a tree starting from the given target file.
|
|
548
|
+
*
|
|
549
|
+
* @param {string} manifestPath - Path to amxbuild.yml (auto-detects if directory)
|
|
550
|
+
* @param {string} targetPath - Path to the .sma or .inc file to build tree from
|
|
551
|
+
* @param {object} [options]
|
|
552
|
+
* @param {'down'|'up'|'auto'} [options.direction='auto']
|
|
553
|
+
* - 'down': show everything the target file #includes (transitively)
|
|
554
|
+
* - 'up': show everything that #includes the target file (transitively)
|
|
555
|
+
* - 'auto': 'down' for .sma, 'up' for .inc
|
|
556
|
+
* @param {number} [options.depth=0] - Max depth (0 = unlimited)
|
|
557
|
+
* @param {string} [options.token] - GitHub PAT
|
|
558
|
+
* @param {boolean} [options.noFetch=false] - Skip network, use cache only
|
|
559
|
+
* @param {'text'|'json'} [options.format='text']
|
|
560
|
+
* @returns {Promise<{ text: string, tree: object, graph: IncludeGraph }>}
|
|
561
|
+
*/
|
|
562
|
+
async function buildIncludeTree(manifestPath, targetPath, options = {}) {
|
|
563
|
+
const direction = options.direction || 'auto';
|
|
564
|
+
const depth = options.depth || 0;
|
|
565
|
+
const token = options.token || null; // explicit override; otherwise per-owner via manifest
|
|
566
|
+
const noFetch = options.noFetch !== undefined ? !!options.noFetch : false;
|
|
567
|
+
const format = options.format || 'text';
|
|
568
|
+
|
|
569
|
+
// ── 1. Resolve manifest ──────────────────────────────────────────────
|
|
570
|
+
const { path: mPath } = resolveManifestPath(manifestPath);
|
|
571
|
+
loadEnv(mPath); // .env tokens (GITHUB_TOKEN etc.)
|
|
572
|
+
const manifest = parseManifest(mPath);
|
|
573
|
+
const manifestDir = path.dirname(mPath);
|
|
574
|
+
|
|
575
|
+
const tokenFor = (repo) => token || resolveGithubToken(manifest, repo);
|
|
576
|
+
|
|
577
|
+
// Resolve refs for every manifest repo once (same single-source loop as the
|
|
578
|
+
// build pipeline); a repo whose ref resolution throws keeps _resolvedRef
|
|
579
|
+
// undefined and is skipped below (ref-less repos still clone default branch).
|
|
580
|
+
await Promise.all(manifest.repos.map(async (repoConfig) => {
|
|
581
|
+
try {
|
|
582
|
+
await resolveRepoRefs([repoConfig], tokenFor);
|
|
583
|
+
} catch (_) { /* ref failed — repo skipped by the fetch loops below */ }
|
|
584
|
+
}));
|
|
585
|
+
|
|
586
|
+
// ── 2. Create graph ──────────────────────────────────────────────────
|
|
587
|
+
const graph = new IncludeGraph();
|
|
588
|
+
|
|
589
|
+
// ── 3. Build include directories list ────────────────────────────────
|
|
590
|
+
// Dep includes come BEFORE the stdlib, matching the real build's search
|
|
591
|
+
// order (src/commands/build.js: deps first, then the compiler bundle).
|
|
592
|
+
|
|
593
|
+
// 3a. Dependency includes — globalDeps + per-repo DEPS_LIST/deps_override
|
|
594
|
+
const depEntries = [...manifest.globalDeps];
|
|
595
|
+
for (const repoConfig of manifest.repos) {
|
|
596
|
+
if (repoConfig.deps_override) {
|
|
597
|
+
depEntries.push(...repoConfig.deps_override);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (repoConfig._resolvedRef === undefined) continue; // ref failed → skip
|
|
601
|
+
try {
|
|
602
|
+
const repoDir = await fetchRepo(repoConfig.repo, repoConfig._resolvedRef, tokenFor(repoConfig.repo), noFetch, manifest.github.ssh);
|
|
603
|
+
const depsPath = path.join(repoDir, 'DEPS_LIST');
|
|
604
|
+
if (fs.existsSync(depsPath)) {
|
|
605
|
+
depEntries.push(...parseDepsLines(fs.readFileSync(depsPath, 'utf8').split(/\r?\n/)));
|
|
606
|
+
}
|
|
607
|
+
} catch (_) { /* skip unresolvable repos */ }
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const seenDeps = new Set();
|
|
611
|
+
for (const dep of depEntries) {
|
|
612
|
+
const key = `${normalize(dep.repo)}@${dep.ref}`;
|
|
613
|
+
if (seenDeps.has(key)) continue;
|
|
614
|
+
seenDeps.add(key);
|
|
615
|
+
try {
|
|
616
|
+
const depDir = await fetchDepIncludeDir(dep, tokenFor(dep.repo), noFetch, manifest.github.ssh);
|
|
617
|
+
graph.includeDirs.push({ path: depDir, label: `dep: ${dep.repo}@${dep.ref}` });
|
|
618
|
+
} catch (_) { /* skip unresolvable */ }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 3b. Standard AMXX includes (compiler bundle)
|
|
622
|
+
const amxVersion = manifest.amxmodx.version || await fetchLatestVersion();
|
|
623
|
+
try {
|
|
624
|
+
const { includeDir } = await fetchCompiler(amxVersion);
|
|
625
|
+
if (includeDir) {
|
|
626
|
+
graph.includeDirs.push({ path: includeDir, label: 'AMXX stdlib ' + amxVersion });
|
|
627
|
+
}
|
|
628
|
+
} catch (_) { /* compiler not available */ }
|
|
629
|
+
|
|
630
|
+
// 3c. Local scripting/ and scripting/include/ — added per-root later
|
|
631
|
+
|
|
632
|
+
// ── 4. Find all root .sma files ──────────────────────────────────────
|
|
633
|
+
|
|
634
|
+
// 4a. Local scripting/
|
|
635
|
+
const localScripting = path.join(manifestDir, manifest.amxmodx.dir, 'scripting');
|
|
636
|
+
const localIncDir = path.join(localScripting, 'include');
|
|
637
|
+
const localLabel = 'local ' + path.relative(manifestDir, localScripting);
|
|
638
|
+
|
|
639
|
+
if (fs.existsSync(localScripting)) {
|
|
640
|
+
// Add scripting/ and scripting/include/ to include dirs
|
|
641
|
+
// for <angle> includes from any file in this project
|
|
642
|
+
graph.includeDirs.push({ path: localScripting, label: localLabel });
|
|
643
|
+
if (fs.existsSync(localIncDir)) {
|
|
644
|
+
graph.includeDirs.push({ path: localIncDir, label: localLabel + '/include' });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Find .sma files
|
|
648
|
+
const localSmas = await glob('**/*.sma', { cwd: localScripting, dot: false });
|
|
649
|
+
for (const rel of localSmas) {
|
|
650
|
+
const abs = path.resolve(localScripting, rel);
|
|
651
|
+
graph.node(abs, {
|
|
652
|
+
name: rel,
|
|
653
|
+
source: localLabel,
|
|
654
|
+
type: 'sma',
|
|
655
|
+
});
|
|
656
|
+
graph.roots.push(abs);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// 4b. Repo scripting/ dirs
|
|
661
|
+
for (const repoConfig of manifest.repos) {
|
|
662
|
+
if (repoConfig._resolvedRef === undefined) continue; // ref failed → skip
|
|
663
|
+
let repoDir;
|
|
664
|
+
try {
|
|
665
|
+
repoDir = await fetchRepo(repoConfig.repo, repoConfig._resolvedRef, tokenFor(repoConfig.repo), noFetch, manifest.github.ssh);
|
|
666
|
+
} catch (_) {
|
|
667
|
+
continue; // skip repos that can't be fetched
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const scriptingDir = path.join(repoDir, repoConfig.amxmodx_dir, 'scripting');
|
|
671
|
+
const incDir = path.join(scriptingDir, 'include');
|
|
672
|
+
const repoLabel = 'repo: ' + repoConfig.repo;
|
|
673
|
+
|
|
674
|
+
if (fs.existsSync(scriptingDir)) {
|
|
675
|
+
// Add scripting/ and scripting/include/ to include dirs
|
|
676
|
+
graph.includeDirs.push({ path: scriptingDir, label: repoLabel });
|
|
677
|
+
if (fs.existsSync(incDir)) {
|
|
678
|
+
graph.includeDirs.push({ path: incDir, label: repoLabel + '/include' });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const smas = await glob('**/*.sma', { cwd: scriptingDir, dot: false });
|
|
682
|
+
for (const rel of smas) {
|
|
683
|
+
const abs = path.resolve(scriptingDir, rel);
|
|
684
|
+
graph.node(abs, {
|
|
685
|
+
name: rel,
|
|
686
|
+
source: repoLabel,
|
|
687
|
+
type: 'sma',
|
|
688
|
+
});
|
|
689
|
+
graph.roots.push(abs);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ── 5. Build the full graph from all roots ───────────────────────────
|
|
695
|
+
graph.build();
|
|
696
|
+
|
|
697
|
+
// ── 6. Determine direction and walk tree from target ─────────────────
|
|
698
|
+
const targetAbs = graph.resolveTarget(targetPath);
|
|
699
|
+
const treeDir = direction === 'auto'
|
|
700
|
+
? (targetAbs.endsWith('.sma') ? 'down' : 'up')
|
|
701
|
+
: direction;
|
|
702
|
+
|
|
703
|
+
let tree;
|
|
704
|
+
if (treeDir === 'up') {
|
|
705
|
+
tree = graph.walkUp(targetAbs);
|
|
706
|
+
} else {
|
|
707
|
+
tree = graph.walkDown(targetAbs);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!tree) {
|
|
711
|
+
const msg = `Target file "${targetPath}" does not exist or could not be read.`;
|
|
712
|
+
return {
|
|
713
|
+
text: msg,
|
|
714
|
+
tree: null,
|
|
715
|
+
graph,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── 7. Format output ─────────────────────────────────────────────────
|
|
720
|
+
const text = format === 'json'
|
|
721
|
+
? JSON.stringify(tree, null, 2)
|
|
722
|
+
: renderTreeText(tree, treeDir, depth);
|
|
723
|
+
|
|
724
|
+
return { text, tree, graph };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Parse a single preprocessor include directive into { filename, localFirst }.
|
|
731
|
+
*
|
|
732
|
+
* Accepted forms:
|
|
733
|
+
* #include <file> — global search only (<> equivalent)
|
|
734
|
+
* #include "file" — local (sma dir) first, then global
|
|
735
|
+
* #include file — bare filename, equivalent to <>
|
|
736
|
+
* <file>, "file", file — directive prefix is optional
|
|
737
|
+
*
|
|
738
|
+
* Extension defaults to .inc if missing.
|
|
739
|
+
* Shared by the MCP resolve_include tool. The IncludeGraph's regex-based
|
|
740
|
+
* parseIncludesFromContent stays the engine for bulk content parsing.
|
|
741
|
+
*/
|
|
742
|
+
function parseIncludeDirective(raw) {
|
|
743
|
+
let input = String(raw || '').trim();
|
|
744
|
+
if (!input) throw new Error('Empty include directive');
|
|
745
|
+
|
|
746
|
+
input = input.replace(/^#include\s+/, '');
|
|
747
|
+
|
|
748
|
+
let localFirst = false;
|
|
749
|
+
|
|
750
|
+
if (input.startsWith('"') && input.endsWith('"')) {
|
|
751
|
+
input = input.slice(1, -1);
|
|
752
|
+
localFirst = true;
|
|
753
|
+
} else if (input.startsWith('<') && input.endsWith('>')) {
|
|
754
|
+
input = input.slice(1, -1);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (!path.extname(input)) input += '.inc';
|
|
758
|
+
|
|
759
|
+
return { filename: input, localFirst };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Search an ordered list of { path, label } targets for a file.
|
|
764
|
+
* Exact match first per target, then case-insensitive fallback (via
|
|
765
|
+
* findCaseInsensitive).
|
|
766
|
+
*
|
|
767
|
+
* Local-first resolution is expressed by ordering `dirs` — the caller pushes
|
|
768
|
+
* the local dir first when the directive was "quoted".
|
|
769
|
+
*
|
|
770
|
+
* @param {{ path: string, label: string }[]} dirs - search targets in priority order
|
|
771
|
+
* @param {string} name - bare filename (may include subdirectories)
|
|
772
|
+
* @returns {{ foundPath: string, label: string } | null}
|
|
773
|
+
*/
|
|
774
|
+
function searchIncludeFile(dirs, name) {
|
|
775
|
+
for (const { path: sp, label } of dirs) {
|
|
776
|
+
const exact = path.join(sp, name);
|
|
777
|
+
if (fs.existsSync(exact)) return { foundPath: exact, label };
|
|
778
|
+
const ci = findCaseInsensitive(sp, name);
|
|
779
|
+
if (ci) return { foundPath: ci, label };
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Collect all .inc files under a directory, sorted, as { rel, abs } entries.
|
|
786
|
+
* Shared glob (dot:false, all levels) used by the MCP dep-interface tools and
|
|
787
|
+
* the include collection in the build pipeline.
|
|
788
|
+
*
|
|
789
|
+
* @param {string} srcDir - directory to scan
|
|
790
|
+
* @returns {Promise<{ rel: string, abs: string }[]>}
|
|
791
|
+
*/
|
|
792
|
+
async function collectIncFiles(srcDir) {
|
|
793
|
+
const entries = await glob('**/*.inc', { cwd: srcDir, dot: false });
|
|
794
|
+
entries.sort();
|
|
795
|
+
return entries.map((rel) => ({ rel, abs: path.join(srcDir, rel) }));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Fetch a dependency's include directory, using cache where possible.
|
|
800
|
+
* Public single-source-of-truth shared by the build pipeline (include-tree),
|
|
801
|
+
* the CLI and the MCP layer.
|
|
802
|
+
*
|
|
803
|
+
* Explicit-include_path semantics: silently falls back to the repo root when
|
|
804
|
+
* the given path does not exist (the MCP callers rely on this).
|
|
805
|
+
*
|
|
806
|
+
* @param {object} dep - { repo, ref, include_path, source, asset }
|
|
807
|
+
* @param {string|null} token - GitHub PAT (per-owner resolved by the caller)
|
|
808
|
+
* @param {boolean} [noFetch=false] - only use cache, skip network
|
|
809
|
+
* @param {boolean} [ssh=false] - clone via SSH
|
|
810
|
+
* @returns {Promise<string>} directory to use as the include dir
|
|
811
|
+
*/
|
|
812
|
+
async function fetchDepIncludeDir(dep, token, noFetch, ssh = false) {
|
|
813
|
+
if (dep.source === 'release') {
|
|
814
|
+
return fetchReleaseDep(
|
|
815
|
+
{ repo: dep.repo, ref: dep.ref, include_path: dep.include_path, asset: dep.asset },
|
|
816
|
+
token,
|
|
817
|
+
noFetch
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const resolvedRef = await resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
822
|
+
const repoDir = await fetchRepo(dep.repo, resolvedRef, token, noFetch, ssh);
|
|
823
|
+
const candidates = dep.include_path
|
|
824
|
+
? [dep.include_path]
|
|
825
|
+
: ['scripting/include', 'amxmodx/scripting/include', 'include', '.'];
|
|
826
|
+
|
|
827
|
+
for (const candidate of candidates) {
|
|
828
|
+
const full = path.join(repoDir, candidate);
|
|
829
|
+
if (fs.existsSync(full)) return full;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return repoDir;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
module.exports = {
|
|
838
|
+
buildIncludeTree,
|
|
839
|
+
IncludeGraph,
|
|
840
|
+
findCaseInsensitive,
|
|
841
|
+
fetchDepIncludeDir,
|
|
842
|
+
parseIncludeDirective,
|
|
843
|
+
searchIncludeFile,
|
|
844
|
+
collectIncFiles,
|
|
845
|
+
};
|