first commit :)
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
.dub
|
||||||
|
docs.json
|
||||||
|
__dummy.html
|
||||||
|
docs/
|
||||||
|
/antelope
|
||||||
|
antelope.so
|
||||||
|
antelope.dylib
|
||||||
|
antelope.dll
|
||||||
|
libantelope.a
|
||||||
|
antelope.a
|
||||||
|
antelope.lib
|
||||||
|
antelope-test-*
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.lst
|
||||||
|
references/
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Initial project scaffolding.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
See the [architecture docs](docs/architecture.md) to understand the design.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2026, Antelope Contributors
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Antelope
|
||||||
|
|
||||||
|
A full replacement for GNU Make, with compatibility in the 99th percentile.
|
||||||
|
Written in D.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Early development.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"description": "A full replacement for GNU Make, with compatability in the 99th percentile. Written in D. Fuck you GNU.",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"authors": [
|
||||||
|
"Specter"
|
||||||
|
],
|
||||||
|
"copyright": "Copyright (c) 2026 Antelope Contributors",
|
||||||
|
"name": "antelope"
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/// Entry point for the Antelope build system.
|
||||||
|
///
|
||||||
|
/// CLI pattern: antelope <subcommand> <options> --[flags]
|
||||||
|
/// Subcommand defaults to `build` when omitted.
|
||||||
|
module antelope.app;
|
||||||
|
|
||||||
|
import std.stdio;
|
||||||
|
import antelope.cli.args;
|
||||||
|
import antelope.cli.subcommands;
|
||||||
|
import antelope.cli.help;
|
||||||
|
import antelope.cli.verinfo;
|
||||||
|
|
||||||
|
int main(string[] args)
|
||||||
|
{
|
||||||
|
// Parse CLI arguments into config
|
||||||
|
CliConfig config = parseArgs(args);
|
||||||
|
|
||||||
|
// Handle help/version quickly before dispatch
|
||||||
|
if (config.showHelp)
|
||||||
|
{
|
||||||
|
printHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (config.showVersion)
|
||||||
|
{
|
||||||
|
printVersion();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch to the appropriate subcommand
|
||||||
|
return dispatchSubcommand(config);
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/// Dependency resolution and ordering logic.
|
||||||
|
///
|
||||||
|
/// Uses Kahn's algorithm (BFS-based topological sort) to produce ordered
|
||||||
|
/// build batches. Each batch contains targets that can be built in parallel.
|
||||||
|
/// The first batch contains leaf targets (no unresolved prereqs in the
|
||||||
|
/// graph), and the last batch contains the requested root target.
|
||||||
|
module antelope.build.dependency;
|
||||||
|
|
||||||
|
import antelope.build.graph;
|
||||||
|
import antelope.build.target;
|
||||||
|
import antelope.diagnostics.errors;
|
||||||
|
|
||||||
|
/// Resolve the full transitive closure of prerequisites and return an
|
||||||
|
/// array of build batches using Kahn's algorithm.
|
||||||
|
///
|
||||||
|
/// Each batch is a slice of targets that can be built in parallel — all
|
||||||
|
/// of their in-graph prerequisites have been satisfied by earlier batches.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// graph = The dependency graph containing all known targets.
|
||||||
|
/// target = The root build target to resolve dependencies for.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// An array of batches `Target[][]` ordered from leaf to root.
|
||||||
|
/// Returns an empty array if the target is not found in the graph.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// target `program` with deps `main.o` → `main.c`, `util.o` → `util.c`
|
||||||
|
/// Returns: `[[main.c, util.c], [main.o, util.o], [program]]`
|
||||||
|
Target[][] resolveDependencies(DependencyGraph graph, string target)
|
||||||
|
{
|
||||||
|
// 1. Find the root target
|
||||||
|
auto rootTarget = graph.findTarget(target);
|
||||||
|
if (rootTarget is null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
// 2. Compute transitive closure — only follow prereqs that exist
|
||||||
|
// as graph targets. Missing prereqs are treated as external files
|
||||||
|
// (already satisfied, don't contribute to in-degree).
|
||||||
|
bool[string] inClosure;
|
||||||
|
string[] stack = [target];
|
||||||
|
|
||||||
|
while (stack.length > 0)
|
||||||
|
{
|
||||||
|
string current = stack[$ - 1];
|
||||||
|
stack = stack[0 .. $ - 1];
|
||||||
|
|
||||||
|
if (current in inClosure)
|
||||||
|
continue;
|
||||||
|
inClosure[current] = true;
|
||||||
|
|
||||||
|
auto tp = graph.findTarget(current);
|
||||||
|
if (tp is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Only follow prereqs that exist as graph targets.
|
||||||
|
// External prereqs (source files, etc.) are treated as
|
||||||
|
// already-satisfied and do not trigger stub creation.
|
||||||
|
foreach (prereq; tp.prerequisites)
|
||||||
|
{
|
||||||
|
if (prereq !in inClosure && graph.hasTarget(prereq))
|
||||||
|
stack ~= prereq;
|
||||||
|
}
|
||||||
|
foreach (prereq; tp.orderOnlyPrereqs)
|
||||||
|
{
|
||||||
|
if (prereq !in inClosure && graph.hasTarget(prereq))
|
||||||
|
stack ~= prereq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Build a fast name → Target lookup for graph targets in the closure.
|
||||||
|
Target[string] targetMap;
|
||||||
|
foreach (ref t; graph.targets)
|
||||||
|
if (t.name in inClosure)
|
||||||
|
targetMap[t.name] = t;
|
||||||
|
|
||||||
|
// 4. Compute in-degree for each target: the number of its prereqs
|
||||||
|
// that are also targets in the graph (and therefore need building).
|
||||||
|
size_t[string] inDegree;
|
||||||
|
string[][string] dependents; // prereq → list of dependents
|
||||||
|
|
||||||
|
foreach (name; inClosure.keys)
|
||||||
|
inDegree[name] = 0;
|
||||||
|
|
||||||
|
foreach (name, ref tgt; targetMap)
|
||||||
|
{
|
||||||
|
size_t unresolved;
|
||||||
|
foreach (prereq; tgt.prerequisites)
|
||||||
|
{
|
||||||
|
if (prereq in targetMap)
|
||||||
|
{
|
||||||
|
unresolved++;
|
||||||
|
dependents[prereq] ~= name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Order-only prereqs also need building
|
||||||
|
foreach (prereq; tgt.orderOnlyPrereqs)
|
||||||
|
{
|
||||||
|
if (prereq in targetMap)
|
||||||
|
{
|
||||||
|
unresolved++;
|
||||||
|
dependents[prereq] ~= name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inDegree[name] = unresolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Kahn's algorithm — process in batches.
|
||||||
|
Target[][] batches;
|
||||||
|
string[] currentBatch;
|
||||||
|
|
||||||
|
// Seed with all nodes that have no unresolved in-graph prereqs.
|
||||||
|
foreach (name; inClosure.keys)
|
||||||
|
{
|
||||||
|
if (inDegree[name] == 0)
|
||||||
|
currentBatch ~= name;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (currentBatch.length > 0)
|
||||||
|
{
|
||||||
|
Target[] batch;
|
||||||
|
foreach (name; currentBatch)
|
||||||
|
{
|
||||||
|
auto tp = name in targetMap;
|
||||||
|
if (tp !is null)
|
||||||
|
batch ~= *tp;
|
||||||
|
}
|
||||||
|
if (batch.length > 0)
|
||||||
|
batches ~= batch;
|
||||||
|
|
||||||
|
// Decrement in-degree for all dependents of the current batch.
|
||||||
|
string[] nextBatch;
|
||||||
|
foreach (name; currentBatch)
|
||||||
|
{
|
||||||
|
auto deps = name in dependents;
|
||||||
|
if (deps is null)
|
||||||
|
continue;
|
||||||
|
foreach (dep; *deps)
|
||||||
|
{
|
||||||
|
inDegree[dep]--;
|
||||||
|
if (inDegree[dep] == 0)
|
||||||
|
nextBatch ~= dep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentBatch = nextBatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. If any nodes still have inDegree > 0, there's a cycle.
|
||||||
|
// Those targets will never reach batch 0 and won't appear in
|
||||||
|
// the output — the caller should detect that some targets are
|
||||||
|
// missing from the batches.
|
||||||
|
return batches;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// Build a test graph:
|
||||||
|
// program → main.o → main.c
|
||||||
|
// program → util.o → util.c
|
||||||
|
DependencyGraph g;
|
||||||
|
g.addTarget(Target("main.c", TargetKind.file, [], []));
|
||||||
|
g.addTarget(Target("util.c", TargetKind.file, [], []));
|
||||||
|
g.addTarget(Target("main.o", TargetKind.file, ["main.c"], ["gcc -c main.c"]));
|
||||||
|
g.addTarget(Target("util.o", TargetKind.file, ["util.c"], ["gcc -c util.c"]));
|
||||||
|
g.addTarget(Target("program", TargetKind.file,
|
||||||
|
["main.o", "util.o"], ["gcc -o program main.o util.o"]));
|
||||||
|
|
||||||
|
auto batches = resolveDependencies(g, "program");
|
||||||
|
|
||||||
|
// Expected: [[main.c, util.c], [main.o, util.o], [program]]
|
||||||
|
assert(batches.length == 3);
|
||||||
|
assert(batches[0].length == 2);
|
||||||
|
assert(batches[1].length == 2);
|
||||||
|
assert(batches[2].length == 1);
|
||||||
|
assert(batches[2][0].name == "program");
|
||||||
|
|
||||||
|
// Leaf batch can be in any order, but both leaves must be present.
|
||||||
|
bool hasMainC, hasUtilC;
|
||||||
|
foreach (t; batches[0])
|
||||||
|
{
|
||||||
|
if (t.name == "main.c") hasMainC = true;
|
||||||
|
if (t.name == "util.c") hasUtilC = true;
|
||||||
|
}
|
||||||
|
assert(hasMainC && hasUtilC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression: missing target returns empty.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
DependencyGraph g;
|
||||||
|
auto batches = resolveDependencies(g, "nonexistent");
|
||||||
|
assert(batches.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression: external prerequisite (not in graph) is treated as already
|
||||||
|
/// satisfied and does not contribute to in-degree.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
DependencyGraph g;
|
||||||
|
// main.o depends on main.c, but main.c is NOT in the graph
|
||||||
|
g.addTarget(Target("main.o", TargetKind.file, ["main.c"], ["gcc -c main.c"]));
|
||||||
|
g.addTarget(Target("program", TargetKind.file, ["main.o"], ["gcc -o program main.o"]));
|
||||||
|
|
||||||
|
auto batches = resolveDependencies(g, "program");
|
||||||
|
// main.c is external → main.o has effective in-degree 0
|
||||||
|
// Expected: [[main.o], [program]]
|
||||||
|
assert(batches.length == 2);
|
||||||
|
assert(batches[0].length == 1);
|
||||||
|
assert(batches[0][0].name == "main.o");
|
||||||
|
assert(batches[1].length == 1);
|
||||||
|
assert(batches[1][0].name == "program");
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/// Command execution engine — runs recipe lines and reports results.
|
||||||
|
module antelope.build.executor;
|
||||||
|
|
||||||
|
import antelope.shell.process;
|
||||||
|
|
||||||
|
/// Result of executing a single recipe line.
|
||||||
|
struct ExecResult
|
||||||
|
{
|
||||||
|
bool success; /// True if the command succeeded or ignoreErrors was set
|
||||||
|
string output; /// (reserved for future output capture)
|
||||||
|
int exitCode; /// Exit code from the process
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a command string and return the result.
|
||||||
|
///
|
||||||
|
/// Handles GNU Make recipe prefix characters (@, -, +), then passes the
|
||||||
|
/// remaining line directly to /bin/sh. Tokenization is deliberately
|
||||||
|
/// avoided — the shell interprets metacharacters (;, >, <, |, &&, ||)
|
||||||
|
/// that would be broken by argument-level quoting.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// command = The raw recipe line text (may include prefix chars)
|
||||||
|
/// environment = Optional environment variables (KEY=VALUE) to pass to
|
||||||
|
/// the subprocess. If empty, the parent env is inherited.
|
||||||
|
///
|
||||||
|
/// Returns: ExecResult with success flag, output, and exit code.
|
||||||
|
ExecResult execute(string command, string[] environment = [])
|
||||||
|
{
|
||||||
|
import std.string : stripLeft;
|
||||||
|
|
||||||
|
string trimmed = command.stripLeft();
|
||||||
|
if (trimmed.length == 0)
|
||||||
|
return ExecResult(true, "", 0);
|
||||||
|
|
||||||
|
// Strip GNU Make prefix characters (@, -, +) from the start of the
|
||||||
|
// line. These flags affect behaviour; the rest of the line is
|
||||||
|
// passed verbatim to the shell.
|
||||||
|
bool ignoreErrors;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (trimmed.length == 0) break;
|
||||||
|
switch (trimmed[0])
|
||||||
|
{
|
||||||
|
case '@': trimmed = trimmed[1..$]; continue;
|
||||||
|
case '-': ignoreErrors = true; trimmed = trimmed[1..$]; continue;
|
||||||
|
case '+': trimmed = trimmed[1..$]; continue;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length == 0)
|
||||||
|
return ExecResult(true, "", 0);
|
||||||
|
|
||||||
|
// Execute via /bin/sh — shells handle metacharacters natively.
|
||||||
|
int code = runProcess(trimmed, environment);
|
||||||
|
|
||||||
|
// Success if exit code is 0 OR the ignoreErrors (-) flag is set
|
||||||
|
bool ok = (code == 0 || ignoreErrors);
|
||||||
|
return ExecResult(ok, "", code);
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
/// Dependency graph construction from parsed rules.
|
||||||
|
module antelope.build.graph;
|
||||||
|
|
||||||
|
import antelope.parser.ast;
|
||||||
|
import antelope.build.target;
|
||||||
|
import antelope.diagnostics.errors;
|
||||||
|
|
||||||
|
/// A directed graph of target → prerequisite relationships.
|
||||||
|
struct DependencyGraph
|
||||||
|
{
|
||||||
|
Target[] targets; /// All known targets.
|
||||||
|
string[string] variables; /// Parsed variable assignments.
|
||||||
|
AntelopeError[] cycleErrors; /// Cycle detection results.
|
||||||
|
bool[string] phonyTargets; /// Names marked as .PHONY.
|
||||||
|
|
||||||
|
/// Build and return the dependency graph from the AST.
|
||||||
|
static DependencyGraph fromAst(AstNode root)
|
||||||
|
{
|
||||||
|
DependencyGraph graph;
|
||||||
|
|
||||||
|
// Walk top-level rule_list children: rules, variable assignments,
|
||||||
|
// directives, etc.
|
||||||
|
foreach (child; root.children)
|
||||||
|
{
|
||||||
|
final switch (child.type)
|
||||||
|
{
|
||||||
|
case AstType.rule:
|
||||||
|
graph.addRuleFromAst(child);
|
||||||
|
break;
|
||||||
|
case AstType.variable_assignment:
|
||||||
|
// data holds "name=value" — store for later expansion
|
||||||
|
graph.parseVariableAssignment(child.data);
|
||||||
|
break;
|
||||||
|
case AstType.directive:
|
||||||
|
// Directives (include, vpath, etc.) are handled
|
||||||
|
// by the evaluator layer — skip for now.
|
||||||
|
break;
|
||||||
|
case AstType.rule_list:
|
||||||
|
// Nested rule_list (e.g., from include merging).
|
||||||
|
// Recurse into it.
|
||||||
|
foreach (nested; child.children)
|
||||||
|
{
|
||||||
|
if (nested.type == AstType.rule)
|
||||||
|
graph.addRuleFromAst(nested);
|
||||||
|
else if (nested.type == AstType.variable_assignment)
|
||||||
|
graph.parseVariableAssignment(nested.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AstType.prerequisite:
|
||||||
|
case AstType.recipe_line:
|
||||||
|
case AstType.function_call:
|
||||||
|
// Should not appear as direct children of rule_list.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .PHONY handling: mark its prerequisites as phony targets
|
||||||
|
graph.handlePhony();
|
||||||
|
|
||||||
|
// Cycle detection: DFS with three-color marking
|
||||||
|
graph.detectCycles();
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a target to the graph.
|
||||||
|
void addTarget(Target t)
|
||||||
|
{
|
||||||
|
targets ~= t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a target by name. Returns null if not found.
|
||||||
|
Target* findTarget(string name)
|
||||||
|
{
|
||||||
|
foreach (ref t; targets)
|
||||||
|
if (t.name == name) return &t;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a target exists in the graph.
|
||||||
|
bool hasTarget(string name)
|
||||||
|
{
|
||||||
|
return findTarget(name) !is null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Extract target name, prerequisites, and recipe from a rule AST node.
|
||||||
|
void addRuleFromAst(AstNode ruleNode)
|
||||||
|
{
|
||||||
|
string targetName = ruleNode.data;
|
||||||
|
string[] prereqs;
|
||||||
|
string[] recipe;
|
||||||
|
|
||||||
|
foreach (child; ruleNode.children)
|
||||||
|
{
|
||||||
|
if (child.type == AstType.prerequisite)
|
||||||
|
prereqs ~= child.data;
|
||||||
|
else if (child.type == AstType.recipe_line)
|
||||||
|
recipe ~= child.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Target t;
|
||||||
|
t.name = targetName;
|
||||||
|
t.kind = TargetKind.file;
|
||||||
|
t.prerequisites = prereqs;
|
||||||
|
t.recipe = recipe;
|
||||||
|
addTarget(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a "name=value" variable assignment string into the variables map.
|
||||||
|
void parseVariableAssignment(string data)
|
||||||
|
{
|
||||||
|
import std.string : indexOf;
|
||||||
|
auto eq = data.indexOf('=');
|
||||||
|
if (eq > 0)
|
||||||
|
{
|
||||||
|
string name = data[0 .. eq];
|
||||||
|
string value = data[eq + 1 .. $];
|
||||||
|
variables[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the ".PHONY" target (if it exists) and mark all of its
|
||||||
|
/// prerequisites as phony targets.
|
||||||
|
void handlePhony()
|
||||||
|
{
|
||||||
|
Target* phony = findTarget(".PHONY");
|
||||||
|
if (phony is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (name; phony.prerequisites)
|
||||||
|
{
|
||||||
|
Target* t = findTarget(name);
|
||||||
|
if (t !is null)
|
||||||
|
t.kind = TargetKind.phony;
|
||||||
|
phonyTargets[name] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect cycles in the dependency graph using three-color DFS.
|
||||||
|
/// Stores found cycles as AntelopeError in cycleErrors.
|
||||||
|
void detectCycles()
|
||||||
|
{
|
||||||
|
// Three colors for DFS state.
|
||||||
|
enum Color : ubyte { white, gray, black }
|
||||||
|
|
||||||
|
// Adjacency list: target name → prerequisite names.
|
||||||
|
string[][string] adjacency;
|
||||||
|
foreach (t; targets)
|
||||||
|
adjacency[t.name] = t.prerequisites;
|
||||||
|
|
||||||
|
Color[string] colors; // Default-initialized to white (0).
|
||||||
|
string[] stack; // Current DFS path for cycle reporting.
|
||||||
|
|
||||||
|
// Recursive DFS visit; returns true if a cycle was found.
|
||||||
|
bool dfsVisit(string node)
|
||||||
|
{
|
||||||
|
colors[node] = Color.gray;
|
||||||
|
stack ~= node;
|
||||||
|
|
||||||
|
auto depsPtr = node in adjacency;
|
||||||
|
if (depsPtr)
|
||||||
|
{
|
||||||
|
foreach (dep; *depsPtr)
|
||||||
|
{
|
||||||
|
Color* cPtr = dep in colors;
|
||||||
|
|
||||||
|
if (cPtr is null || *cPtr == Color.white)
|
||||||
|
{
|
||||||
|
// Not yet visited — descend.
|
||||||
|
if (dfsVisit(dep))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (*cPtr == Color.gray)
|
||||||
|
{
|
||||||
|
// Back-edge found — construct cycle description.
|
||||||
|
// Find where `dep` first appears in stack.
|
||||||
|
import std.string : join;
|
||||||
|
ptrdiff_t cycleStart = -1;
|
||||||
|
foreach (i, s; stack)
|
||||||
|
{
|
||||||
|
if (s == dep)
|
||||||
|
{
|
||||||
|
cycleStart = cast(ptrdiff_t) i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string[] cyclePath = stack[cast(size_t) cycleStart .. $];
|
||||||
|
cyclePath ~= dep; // Close the loop.
|
||||||
|
|
||||||
|
string cycleMsg = "cycle: " ~ cyclePath.join(" \u2192 ");
|
||||||
|
cycleErrors ~= AntelopeError(
|
||||||
|
ErrorKind.cyclicDependency,
|
||||||
|
cycleMsg,
|
||||||
|
"", 0, 0
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// black nodes are already fully processed — ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colors[node] = Color.black;
|
||||||
|
stack = stack[0 .. $ - 1]; // Pop.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (t; targets)
|
||||||
|
{
|
||||||
|
if (!(t.name in colors))
|
||||||
|
dfsVisit(t.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Unittests ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Two-rule graph with no cycles.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// AST: all: program
|
||||||
|
// ./program
|
||||||
|
// program: main.o
|
||||||
|
// cc -o program main.o
|
||||||
|
|
||||||
|
auto all = AstNode(AstType.rule, [], "all");
|
||||||
|
all.children ~= AstNode(AstType.prerequisite, [], "program");
|
||||||
|
all.children ~= AstNode(AstType.recipe_line, [], "./program");
|
||||||
|
|
||||||
|
auto program = AstNode(AstType.rule, [], "program");
|
||||||
|
program.children ~= AstNode(AstType.prerequisite, [], "main.o");
|
||||||
|
program.children ~= AstNode(AstType.recipe_line, [], "cc -o program main.o");
|
||||||
|
|
||||||
|
auto root = AstNode(AstType.rule_list, [all, program], "");
|
||||||
|
|
||||||
|
auto graph = DependencyGraph.fromAst(root);
|
||||||
|
|
||||||
|
assert(graph.targets.length == 2);
|
||||||
|
assert(graph.hasTarget("all"));
|
||||||
|
assert(graph.hasTarget("program"));
|
||||||
|
|
||||||
|
auto allTarget = graph.findTarget("all");
|
||||||
|
assert(allTarget !is null);
|
||||||
|
assert(allTarget.prerequisites == ["program"]);
|
||||||
|
assert(allTarget.recipe == ["./program"]);
|
||||||
|
|
||||||
|
// No cycles expected.
|
||||||
|
assert(graph.cycleErrors.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle detection: a → b → c → a.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// AST: a: b
|
||||||
|
// b: c
|
||||||
|
// c: a
|
||||||
|
|
||||||
|
auto a = AstNode(AstType.rule, [], "a");
|
||||||
|
a.children ~= AstNode(AstType.prerequisite, [], "b");
|
||||||
|
|
||||||
|
auto b = AstNode(AstType.rule, [], "b");
|
||||||
|
b.children ~= AstNode(AstType.prerequisite, [], "c");
|
||||||
|
|
||||||
|
auto c = AstNode(AstType.rule, [], "c");
|
||||||
|
c.children ~= AstNode(AstType.prerequisite, [], "a");
|
||||||
|
|
||||||
|
auto root = AstNode(AstType.rule_list, [a, b, c], "");
|
||||||
|
|
||||||
|
auto graph = DependencyGraph.fromAst(root);
|
||||||
|
|
||||||
|
// Graph should still be built with 3 targets.
|
||||||
|
assert(graph.targets.length == 3);
|
||||||
|
|
||||||
|
// Cycle must be detected.
|
||||||
|
assert(graph.cycleErrors.length > 0);
|
||||||
|
assert(graph.cycleErrors[0].kind == ErrorKind.cyclicDependency);
|
||||||
|
}
|
||||||
|
|
||||||
|
// .PHONY handling: mark prerequisites as phony.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// AST: .PHONY: clean
|
||||||
|
// clean:
|
||||||
|
// rm -f *.o
|
||||||
|
|
||||||
|
auto phony = AstNode(AstType.rule, [], ".PHONY");
|
||||||
|
phony.children ~= AstNode(AstType.prerequisite, [], "clean");
|
||||||
|
|
||||||
|
auto clean = AstNode(AstType.rule, [], "clean");
|
||||||
|
clean.children ~= AstNode(AstType.recipe_line, [], "rm -f *.o");
|
||||||
|
|
||||||
|
auto root = AstNode(AstType.rule_list, [phony, clean], "");
|
||||||
|
|
||||||
|
auto graph = DependencyGraph.fromAst(root);
|
||||||
|
|
||||||
|
assert(graph.targets.length == 2);
|
||||||
|
|
||||||
|
auto cleanTarget = graph.findTarget("clean");
|
||||||
|
assert(cleanTarget !is null);
|
||||||
|
assert(cleanTarget.kind == TargetKind.phony);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/// Build job scheduler — decides execution order and parallelism.
|
||||||
|
///
|
||||||
|
/// Uses Kahn's-algorithm topological sort (via `resolveDependencies`) to
|
||||||
|
/// determine build order, then filters targets that actually need rebuilding
|
||||||
|
/// by comparing file timestamps.
|
||||||
|
module antelope.build.scheduler;
|
||||||
|
|
||||||
|
import antelope.build.graph;
|
||||||
|
import antelope.build.dependency;
|
||||||
|
import antelope.build.target;
|
||||||
|
import antelope.filesystem.timestamps;
|
||||||
|
|
||||||
|
/// Available scheduler strategies.
|
||||||
|
///
|
||||||
|
/// The strategy controls how the build order is consumed by the executor:
|
||||||
|
/// serial — Process targets one at a time in dependency order.
|
||||||
|
/// parallel — Build targets within each topological batch concurrently.
|
||||||
|
/// topological — Return the pure dependency order without filtering
|
||||||
|
/// for up-to-date targets (useful for inspection).
|
||||||
|
///
|
||||||
|
/// The current implementation always uses topological batching; the
|
||||||
|
/// strategy is threaded through so the executor can decide whether to
|
||||||
|
/// run batches serially or in parallel.
|
||||||
|
enum SchedulerStrategy
|
||||||
|
{
|
||||||
|
serial,
|
||||||
|
parallel,
|
||||||
|
topological,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule and return an ordered list of target names to build.
|
||||||
|
///
|
||||||
|
/// Steps:
|
||||||
|
/// 1. Determine the root target — tries "all" (GNU Make convention),
|
||||||
|
/// falls back to the first target in the graph.
|
||||||
|
/// 2. Run Kahn's topological sort to produce build batches.
|
||||||
|
/// 3. For each target, check `needsRebuild()` against its prerequisites
|
||||||
|
/// to decide whether it actually needs to be built.
|
||||||
|
/// 4. Flatten batches into a single ordered `string[]`.
|
||||||
|
///
|
||||||
|
/// Targets that are up-to-date (target file exists and is newer than all
|
||||||
|
/// prereqs) are skipped.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// graph = The dependency graph containing all known targets.
|
||||||
|
/// strategy = How the executor should consume the output (serial,
|
||||||
|
/// parallel, or raw topological).
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// Ordered list of target names that need building. Returns an empty
|
||||||
|
/// array when the graph has no targets or the root target is untraceable.
|
||||||
|
string[] schedule(DependencyGraph graph, SchedulerStrategy strategy)
|
||||||
|
{
|
||||||
|
// 1. Pick the default target.
|
||||||
|
string targetName;
|
||||||
|
if (graph.hasTarget("all"))
|
||||||
|
targetName = "all";
|
||||||
|
else if (graph.targets.length > 0)
|
||||||
|
targetName = graph.targets[0].name;
|
||||||
|
else
|
||||||
|
return [];
|
||||||
|
|
||||||
|
// 2. Resolve full dependency tree into topological batches.
|
||||||
|
Target[][] batches = resolveDependencies(graph, targetName);
|
||||||
|
if (batches.length == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
// 3. Collect targets that actually need building.
|
||||||
|
string[] buildOrder;
|
||||||
|
|
||||||
|
foreach (batch; batches)
|
||||||
|
{
|
||||||
|
foreach (ref tgt; batch)
|
||||||
|
{
|
||||||
|
// In topological mode, include every target regardless of
|
||||||
|
// freshness (useful for inspection / dry-run analysis).
|
||||||
|
final switch (strategy)
|
||||||
|
{
|
||||||
|
case SchedulerStrategy.topological:
|
||||||
|
buildOrder ~= tgt.name;
|
||||||
|
break;
|
||||||
|
case SchedulerStrategy.serial:
|
||||||
|
case SchedulerStrategy.parallel:
|
||||||
|
if (needsRebuild(tgt.name, tgt.prerequisites, &graph.phonyTargets))
|
||||||
|
buildOrder ~= tgt.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// Simple chain: leaf1, leaf2 → middle.
|
||||||
|
// middle must be the first target added (and thus the default)
|
||||||
|
// so that the scheduler picks it up as the root.
|
||||||
|
DependencyGraph g;
|
||||||
|
g.addTarget(Target("middle", TargetKind.file, ["leaf1", "leaf2"], []));
|
||||||
|
g.addTarget(Target("leaf1", TargetKind.file, [], []));
|
||||||
|
g.addTarget(Target("leaf2", TargetKind.file, [], []));
|
||||||
|
|
||||||
|
// All targets should need rebuilding (files don't exist on disk).
|
||||||
|
auto order = schedule(g, SchedulerStrategy.serial);
|
||||||
|
assert(order.length == 3);
|
||||||
|
|
||||||
|
// In topological mode, no filtering is applied.
|
||||||
|
auto topoOrder = schedule(g, SchedulerStrategy.topological);
|
||||||
|
assert(topoOrder.length == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression: "all" target takes priority as the default root.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
DependencyGraph g;
|
||||||
|
g.addTarget(Target("somethingElse", TargetKind.file, [], []));
|
||||||
|
g.addTarget(Target("all", TargetKind.phony, ["somethingElse"], []));
|
||||||
|
|
||||||
|
auto order = schedule(g, SchedulerStrategy.serial);
|
||||||
|
// All targets should be in the build order (phony + file).
|
||||||
|
assert(order.length == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression: empty graph returns empty order.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
DependencyGraph g;
|
||||||
|
auto order = schedule(g, SchedulerStrategy.serial);
|
||||||
|
assert(order.length == 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/// Target representation — files, phony targets, and their metadata.
|
||||||
|
module antelope.build.target;
|
||||||
|
|
||||||
|
/// What kind of target this is.
|
||||||
|
enum TargetKind
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
phony,
|
||||||
|
intermediate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single build target.
|
||||||
|
struct Target
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
TargetKind kind;
|
||||||
|
string[] prerequisites; /// Normal prerequisites (trigger rebuild)
|
||||||
|
string[] recipe; /// Shell commands to build this target
|
||||||
|
string[] orderOnlyPrereqs; /// Order-only prerequisites (| — must exist, no rebuild trigger)
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
/// Command-line argument parsing.
|
||||||
|
///
|
||||||
|
/// Pattern: antelope <subcommand> <options> --[flags]
|
||||||
|
/// Subcommand defaults to `build` when omitted.
|
||||||
|
module antelope.cli.args;
|
||||||
|
|
||||||
|
import std.algorithm.searching : startsWith;
|
||||||
|
import std.conv : to;
|
||||||
|
import std.string : strip;
|
||||||
|
import std.ascii : isDigit;
|
||||||
|
|
||||||
|
/// Available subcommands.
|
||||||
|
enum Subcommand
|
||||||
|
{
|
||||||
|
build, /// Run the build (default)
|
||||||
|
hunt, /// Makefile → Antefile converter (late-stage)
|
||||||
|
configure, /// Autotools configure.ac handler (future)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed CLI configuration.
|
||||||
|
struct CliConfig
|
||||||
|
{
|
||||||
|
/// Which subcommand to run.
|
||||||
|
Subcommand subcommand = Subcommand.build;
|
||||||
|
|
||||||
|
/// Build targets to execute.
|
||||||
|
string[] targets;
|
||||||
|
|
||||||
|
/// Explicit build file (--file / -f <path>).
|
||||||
|
string file;
|
||||||
|
|
||||||
|
/// Enable GNU Make compatibility mode (-gnu / --gnu).
|
||||||
|
/// Enables Makefile/makefile reading, implicit rules,
|
||||||
|
/// automatic variables, VPATH, and all GNU Make semantics.
|
||||||
|
bool gnuMode;
|
||||||
|
|
||||||
|
/// Dry run: show what would be done without executing (-n / --dry-run).
|
||||||
|
bool dryRun;
|
||||||
|
|
||||||
|
/// Debug output (-d / --debug).
|
||||||
|
bool debugMode;
|
||||||
|
|
||||||
|
/// POSIX conformance mode (-P / --posix).
|
||||||
|
bool posix;
|
||||||
|
|
||||||
|
/// Parallel job count (-j <N>). 0 = unlimited, 1 = serial (default).
|
||||||
|
uint jobs = 1;
|
||||||
|
|
||||||
|
/// Change to directory before execution (-C <dir>).
|
||||||
|
string directory;
|
||||||
|
|
||||||
|
/// Show help (--help).
|
||||||
|
bool showHelp;
|
||||||
|
|
||||||
|
/// Show version (--version).
|
||||||
|
bool showVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
/// Check whether a string consists entirely of digit characters.
|
||||||
|
private bool allDigits(string s)
|
||||||
|
{
|
||||||
|
if (s.length == 0)
|
||||||
|
return false;
|
||||||
|
foreach (c; s)
|
||||||
|
{
|
||||||
|
if (!isDigit(c))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a subcommand string into the enum.
|
||||||
|
private Subcommand parseSubcommand(string s)
|
||||||
|
{
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case "build":
|
||||||
|
return Subcommand.build;
|
||||||
|
case "hunt":
|
||||||
|
return Subcommand.hunt;
|
||||||
|
case "configure":
|
||||||
|
return Subcommand.configure;
|
||||||
|
default:
|
||||||
|
return Subcommand.build;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a string names a recognized subcommand.
|
||||||
|
private bool isSubcommand(string s)
|
||||||
|
{
|
||||||
|
return s == "build" || s == "hunt" || s == "configure";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public API ---
|
||||||
|
|
||||||
|
/// Parse command-line arguments into a CliConfig.
|
||||||
|
///
|
||||||
|
/// Pattern: antelope [targets...] -gnu [flags]
|
||||||
|
/// The first positional argument that matches a subcommand name
|
||||||
|
/// (build, hunt, configure) sets the subcommand. Otherwise all
|
||||||
|
/// positionals become build targets.
|
||||||
|
///
|
||||||
|
/// Unknown flags are silently ignored (GNU Make compatibility).
|
||||||
|
CliConfig parseArgs(string[] args)
|
||||||
|
{
|
||||||
|
CliConfig config;
|
||||||
|
bool firstPositional = true;
|
||||||
|
bool doneWithFlags = false;
|
||||||
|
|
||||||
|
for (size_t i = 1; i < args.length; i++)
|
||||||
|
{
|
||||||
|
string arg = args[i];
|
||||||
|
|
||||||
|
// -- stops flag parsing; everything after is a target
|
||||||
|
if (!doneWithFlags && arg == "--")
|
||||||
|
{
|
||||||
|
doneWithFlags = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After --, everything is a target
|
||||||
|
if (doneWithFlags)
|
||||||
|
{
|
||||||
|
config.targets ~= arg;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --help / --version stop immediately
|
||||||
|
if (arg == "--help")
|
||||||
|
{
|
||||||
|
config.showHelp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (arg == "--version")
|
||||||
|
{
|
||||||
|
config.showVersion = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boolean flags
|
||||||
|
if (arg == "-gnu" || arg == "--gnu")
|
||||||
|
{
|
||||||
|
config.gnuMode = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg == "-n" || arg == "--dry-run")
|
||||||
|
{
|
||||||
|
config.dryRun = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg == "-d" || arg == "--debug")
|
||||||
|
{
|
||||||
|
config.debugMode = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg == "-P" || arg == "--posix")
|
||||||
|
{
|
||||||
|
config.posix = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -jN (combined) or -j N (space-separated)
|
||||||
|
if (arg == "-j")
|
||||||
|
{
|
||||||
|
if (i + 1 < args.length && allDigits(args[i + 1]))
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
config.jobs = args[i].to!uint;
|
||||||
|
}
|
||||||
|
// else: missing/ambiguous argument → keep default jobs=1
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("-j") && arg.length > 2)
|
||||||
|
{
|
||||||
|
string numPart = arg[2 .. $];
|
||||||
|
if (allDigits(numPart))
|
||||||
|
{
|
||||||
|
config.jobs = numPart.to!uint;
|
||||||
|
}
|
||||||
|
// else: invalid number → silently ignore (keep default)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -f file or --file file
|
||||||
|
if (arg == "-f" || arg == "--file")
|
||||||
|
{
|
||||||
|
if (i + 1 < args.length)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
config.file = args[i];
|
||||||
|
}
|
||||||
|
// else: missing argument → keep default empty file
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -Cdir (combined) or -C dir (space-separated)
|
||||||
|
if (arg == "-C")
|
||||||
|
{
|
||||||
|
if (i + 1 < args.length)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
config.directory = args[i];
|
||||||
|
}
|
||||||
|
// else: missing argument → keep default empty directory
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("-C") && arg.length > 2)
|
||||||
|
{
|
||||||
|
config.directory = strip(arg[2 .. $]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand detection: only the first positional is checked
|
||||||
|
if (firstPositional)
|
||||||
|
{
|
||||||
|
firstPositional = false;
|
||||||
|
if (isSubcommand(arg))
|
||||||
|
{
|
||||||
|
config.subcommand = parseSubcommand(arg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown flags → silently ignore (GNU Make compatibility)
|
||||||
|
if (arg.startsWith("-"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else is a target
|
||||||
|
config.targets ~= arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Unittests ---
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// Default: empty args
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope"]);
|
||||||
|
assert(c.subcommand == Subcommand.build);
|
||||||
|
assert(c.targets.length == 0);
|
||||||
|
assert(c.jobs == 1);
|
||||||
|
assert(!c.gnuMode);
|
||||||
|
assert(!c.dryRun);
|
||||||
|
assert(!c.debugMode);
|
||||||
|
assert(!c.posix);
|
||||||
|
assert(!c.showHelp);
|
||||||
|
assert(!c.showVersion);
|
||||||
|
assert(c.file.length == 0);
|
||||||
|
assert(c.directory.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -gnu flag
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-gnu"]);
|
||||||
|
assert(c.gnuMode);
|
||||||
|
assert(c.targets.length == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--gnu"]);
|
||||||
|
assert(c.gnuMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -gnu with targets
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-gnu", "all", "clean"]);
|
||||||
|
assert(c.gnuMode);
|
||||||
|
assert(c.targets == ["all", "clean"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets without -gnu
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "release", "-gnu"]);
|
||||||
|
assert(c.gnuMode);
|
||||||
|
assert(c.targets == ["release"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand: build
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "build", "-j4"]);
|
||||||
|
assert(c.subcommand == Subcommand.build);
|
||||||
|
assert(c.jobs == 4);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "build"]);
|
||||||
|
assert(c.subcommand == Subcommand.build);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand: hunt
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "hunt"]);
|
||||||
|
assert(c.subcommand == Subcommand.hunt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand: configure
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "configure"]);
|
||||||
|
assert(c.subcommand == Subcommand.configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -jN combined form
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-j8"]);
|
||||||
|
assert(c.jobs == 8);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-j0"]); // unlimited
|
||||||
|
assert(c.jobs == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -j N space-separated
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-j", "4"]);
|
||||||
|
assert(c.jobs == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -j alone (missing argument) → default
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-j"]);
|
||||||
|
assert(c.jobs == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -j followed by non-numeric → default, next arg not consumed
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-j", "all"]);
|
||||||
|
assert(c.jobs == 1);
|
||||||
|
assert(c.targets == ["all"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -C dir space-separated
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-C", "/tmp"]);
|
||||||
|
assert(c.directory == "/tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -Cdir combined
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-C/tmp"]);
|
||||||
|
assert(c.directory == "/tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -C alone (missing argument) → default
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-C"]);
|
||||||
|
assert(c.directory.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -n / --dry-run
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-n"]);
|
||||||
|
assert(c.dryRun);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--dry-run"]);
|
||||||
|
assert(c.dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -d / --debug
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-d"]);
|
||||||
|
assert(c.debugMode);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--debug"]);
|
||||||
|
assert(c.debugMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -P / --posix
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-P"]);
|
||||||
|
assert(c.posix);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--posix"]);
|
||||||
|
assert(c.posix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -f file / --file file
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-f", "mymakefile"]);
|
||||||
|
assert(c.file == "mymakefile");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--file", "mymakefile"]);
|
||||||
|
assert(c.file == "mymakefile");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -f alone (missing argument) → default
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-f"]);
|
||||||
|
assert(c.file.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --help stops parsing
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--help", "-gnu", "target"]);
|
||||||
|
assert(c.showHelp);
|
||||||
|
assert(!c.gnuMode);
|
||||||
|
assert(c.targets.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --version stops parsing
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--version", "-gnu"]);
|
||||||
|
assert(c.showVersion);
|
||||||
|
assert(!c.gnuMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- separates targets from flags
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "--", "-gnu", "all"]);
|
||||||
|
assert(c.targets == ["-gnu", "all"]);
|
||||||
|
assert(!c.gnuMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- with targets before and after
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "a", "--", "b", "c"]);
|
||||||
|
assert(c.targets == ["a", "b", "c"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown flags silently ignored
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-k", "--keep-going", "target"]);
|
||||||
|
assert(c.targets == ["target"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple flags combined
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-gnu", "-d", "-P", "-n", "-j", "16"]);
|
||||||
|
assert(c.gnuMode);
|
||||||
|
assert(c.debugMode);
|
||||||
|
assert(c.posix);
|
||||||
|
assert(c.dryRun);
|
||||||
|
assert(c.jobs == 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interleaved flags and positionals
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "-gnu", "all", "-j8", "clean"]);
|
||||||
|
assert(c.gnuMode);
|
||||||
|
assert(c.jobs == 8);
|
||||||
|
assert(c.targets == ["all", "clean"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand with other positionals become targets
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "build", "release"]);
|
||||||
|
assert(c.subcommand == Subcommand.build);
|
||||||
|
assert(c.targets == ["release"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "build" as a target name (not first positional)
|
||||||
|
{
|
||||||
|
auto c = parseArgs(["antelope", "release", "build"]);
|
||||||
|
assert(c.subcommand == Subcommand.build); // default
|
||||||
|
assert(c.targets == ["release", "build"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/// Help text display.
|
||||||
|
module antelope.cli.help;
|
||||||
|
|
||||||
|
/// Print usage information.
|
||||||
|
void printHelp()
|
||||||
|
{
|
||||||
|
import std.stdio;
|
||||||
|
writeln("Antelope — a GNU Make replacement and superset.");
|
||||||
|
writeln("Usage: antelope <subcommand> [options] [targets]");
|
||||||
|
writeln();
|
||||||
|
writeln("Subcommands:");
|
||||||
|
writeln(" build Run the build (default)");
|
||||||
|
writeln(" hunt Convert Makefile to Antefile (late-stage)");
|
||||||
|
writeln(" configure Autotools configure.ac replacement (future)");
|
||||||
|
writeln();
|
||||||
|
writeln("Flags (common):");
|
||||||
|
writeln(" -gnu Enable GNU Make compatibility mode");
|
||||||
|
writeln(" -f <file> Use <file> as the build file");
|
||||||
|
writeln(" -j <N> Run <N> jobs in parallel");
|
||||||
|
writeln(" -C <dir> Change to <dir> before executing");
|
||||||
|
writeln(" -n Dry run (print commands, don't execute)");
|
||||||
|
writeln(" -d Enable debug output");
|
||||||
|
writeln(" -P POSIX conformance mode");
|
||||||
|
writeln(" --help Show this help");
|
||||||
|
writeln(" --version Show version");
|
||||||
|
writeln();
|
||||||
|
writeln("Native mode (default): reads antefile or antelope (case-insensitive)");
|
||||||
|
writeln("GNU mode (-gnu): reads GNUmakefile, Makefile, or makefile");
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
/// Subcommand dispatch — routes to the appropriate handler.
|
||||||
|
///
|
||||||
|
/// Antelope's CLI pattern is: antelope <subcommand> <options> --[flags]
|
||||||
|
/// The subcommand is the first positional argument; it defaults to `build`.
|
||||||
|
module antelope.cli.subcommands;
|
||||||
|
|
||||||
|
import antelope.cli.args;
|
||||||
|
import antelope.cli.help;
|
||||||
|
import antelope.cli.verinfo;
|
||||||
|
import antelope.diagnostics.output;
|
||||||
|
import std.file : exists, readText;
|
||||||
|
import std.conv : to;
|
||||||
|
|
||||||
|
/// Dispatch to the correct handler based on CliConfig.subcommand.
|
||||||
|
/// Returns an exit code (0 = success).
|
||||||
|
int dispatchSubcommand(CliConfig config)
|
||||||
|
{
|
||||||
|
final switch (config.subcommand)
|
||||||
|
{
|
||||||
|
case Subcommand.build:
|
||||||
|
return runBuild(config);
|
||||||
|
case Subcommand.hunt:
|
||||||
|
return runHunt(config);
|
||||||
|
case Subcommand.configure:
|
||||||
|
return runConfigure(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the build (default subcommand).
|
||||||
|
///
|
||||||
|
/// Full pipeline: find build file → parse → evaluate → schedule → execute.
|
||||||
|
int runBuild(CliConfig config)
|
||||||
|
{
|
||||||
|
import antelope.parser.parser;
|
||||||
|
import antelope.parser.ast;
|
||||||
|
import antelope.evaluator.evaluator;
|
||||||
|
import antelope.evaluator.expansion;
|
||||||
|
import antelope.build.graph;
|
||||||
|
import antelope.build.target;
|
||||||
|
import antelope.build.dependency;
|
||||||
|
import antelope.build.scheduler;
|
||||||
|
import antelope.build.executor;
|
||||||
|
import antelope.shell.environment;
|
||||||
|
import antelope.filesystem.timestamps;
|
||||||
|
|
||||||
|
// Set log level
|
||||||
|
if (config.debugMode)
|
||||||
|
setLogLevel(LogLevel.dbg);
|
||||||
|
|
||||||
|
// Change to target directory (-C <dir>)
|
||||||
|
if (config.directory.length > 0)
|
||||||
|
{
|
||||||
|
import std.file : chdir;
|
||||||
|
try { chdir(config.directory); }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: cannot chdir to " ~ config.directory ~ ": " ~ e.msg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find build file
|
||||||
|
string buildFile = findBuildFile(config);
|
||||||
|
if (buildFile.length == 0)
|
||||||
|
{
|
||||||
|
if (config.gnuMode)
|
||||||
|
log(LogLevel.normal, "antelope: *** No targets specified and no makefile found. Stop.");
|
||||||
|
else
|
||||||
|
log(LogLevel.normal, "antelope: *** No build file found. Stop.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
log(LogLevel.verbose, "Using build file: " ~ buildFile);
|
||||||
|
|
||||||
|
// Read the build file
|
||||||
|
string content;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
content = readText(buildFile);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: *** Cannot read build file: " ~ buildFile);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse into AST
|
||||||
|
AstNode ast;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ast = parse(content);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: *** Parse error: " ~ e.msg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup environment with OS env
|
||||||
|
auto env = new Environment();
|
||||||
|
{
|
||||||
|
import std.process : environment;
|
||||||
|
env.mergeEnv(environment.toAA());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set MAKE to the antelope binary path for $(MAKE) in recipes.
|
||||||
|
// Include -gnu so recursive sub-makes inherit GNU compat mode.
|
||||||
|
import std.file : thisExePath;
|
||||||
|
string makeCmd = thisExePath();
|
||||||
|
if (config.gnuMode) makeCmd ~= " -gnu";
|
||||||
|
if (config.file.length > 0) makeCmd ~= " -f " ~ config.file;
|
||||||
|
env.set("MAKE", makeCmd);
|
||||||
|
|
||||||
|
// Set MAKECMDGOALS from command-line targets (autotools compat)
|
||||||
|
if (config.targets.length > 0)
|
||||||
|
{
|
||||||
|
import std.string : join;
|
||||||
|
env.set("MAKECMDGOALS", config.targets.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate AST → populate env + build graph
|
||||||
|
auto graph = new DependencyGraph();
|
||||||
|
|
||||||
|
// --- GNU Make compatibility (gnu_make.d) ---
|
||||||
|
import antelope.compatibility.gnu_make;
|
||||||
|
GnuMakeCompat gnuCompat;
|
||||||
|
if (config.gnuMode)
|
||||||
|
gnuCompat = GnuMakeCompat.withDefaults();
|
||||||
|
|
||||||
|
// --- POSIX conformance mode (posix_make.d) ---
|
||||||
|
import antelope.compatibility.posix_make;
|
||||||
|
PosixCompat posixCompat;
|
||||||
|
if (config.posix)
|
||||||
|
posixCompat.mode = PosixConformance.strict;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
evaluate(ast, env, graph, &gnuCompat, &posixCompat);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: *** Evaluation error: " ~ e.msg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cycle errors
|
||||||
|
if (graph.cycleErrors.length > 0)
|
||||||
|
{
|
||||||
|
foreach (err; graph.cycleErrors)
|
||||||
|
log(LogLevel.normal, "antelope: *** " ~ err.message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In -gnu mode (and not POSIX strict), resolve implicit rules for
|
||||||
|
// targets without recipes. POSIX strict mode disables GNU extensions
|
||||||
|
// including implicit rule resolution.
|
||||||
|
// This fills in recipes from the built-in rule database (e.g.
|
||||||
|
// %.o: %.c and %: %.o) so that targets with no explicit recipe
|
||||||
|
// can still be built. Multiple passes handle rule chaining:
|
||||||
|
// "program" → link rule adds "program.o" → compile rule adds
|
||||||
|
// "program.c". Safety cap of 10 passes prevents infinite loops.
|
||||||
|
if (config.gnuMode && !config.posix)
|
||||||
|
{
|
||||||
|
import antelope.evaluator.evaluator : resolveImplicitRules;
|
||||||
|
const size_t maxPasses = 5; // deep enough for .l → .c → .s → .o chains
|
||||||
|
for (size_t pass = 0; pass < maxPasses; pass++)
|
||||||
|
{
|
||||||
|
auto resolved = resolveImplicitRules(*graph, env);
|
||||||
|
log(LogLevel.dbg, "Implicit rule pass " ~
|
||||||
|
(pass + 1).to!string ~ ": " ~ resolved.to!string ~
|
||||||
|
" target(s) resolved");
|
||||||
|
if (resolved == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graph.targets.length == 0)
|
||||||
|
{
|
||||||
|
log(LogLevel.verbose, "No targets defined.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which targets to build
|
||||||
|
string[] buildTargets;
|
||||||
|
if (config.targets.length > 0)
|
||||||
|
buildTargets = config.targets;
|
||||||
|
else if (graph.hasTarget("all"))
|
||||||
|
buildTargets = ["all"];
|
||||||
|
else
|
||||||
|
buildTargets = [graph.targets[0].name];
|
||||||
|
|
||||||
|
// .PHONY targets are tracked in the graph automatically via handlePhony()
|
||||||
|
|
||||||
|
// --- VPATH configuration (GNU Make compat) ---
|
||||||
|
import antelope.compatibility.vpath;
|
||||||
|
VPathConfig vpath;
|
||||||
|
if (config.gnuMode && env.hasKey("VPATH"))
|
||||||
|
{
|
||||||
|
import std.string : split;
|
||||||
|
string vpathVal = env.get("VPATH");
|
||||||
|
foreach (dir; vpathVal.split(":"))
|
||||||
|
{
|
||||||
|
if (dir.length > 0)
|
||||||
|
vpath.globalSearchDirs ~= dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also read pattern-scoped vpath entries stored by handleDirective
|
||||||
|
if (config.gnuMode)
|
||||||
|
{
|
||||||
|
import std.string : split, startsWith;
|
||||||
|
foreach (key; env.keys())
|
||||||
|
{
|
||||||
|
if (key.length > 8 && key[0..8] == "__vpath_")
|
||||||
|
{
|
||||||
|
string pattern = key[8..$];
|
||||||
|
string dirsStr = env.get(key);
|
||||||
|
VPathEntry entry;
|
||||||
|
entry.pattern = pattern;
|
||||||
|
foreach (dir; dirsStr.split(" "))
|
||||||
|
if (dir.length > 0) entry.directories ~= dir;
|
||||||
|
if (entry.directories.length > 0)
|
||||||
|
vpath.patternEntries ~= entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build each requested target
|
||||||
|
int exitCode = 0;
|
||||||
|
foreach (targetName; buildTargets)
|
||||||
|
{
|
||||||
|
if (!graph.hasTarget(targetName))
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: *** No rule to make target '" ~
|
||||||
|
targetName ~ "'. Stop.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve dependencies and check what needs building
|
||||||
|
auto batches = resolveDependencies(*graph, targetName);
|
||||||
|
|
||||||
|
import std.stdio;
|
||||||
|
if (targetName == "libgnu.a" || targetName == "all") {
|
||||||
|
stderr.writefln(" batches=%d", batches.length);
|
||||||
|
foreach (i, batch; batches) {
|
||||||
|
size_t w;
|
||||||
|
foreach (ref t; batch) if (t.recipe.length > 0) w++;
|
||||||
|
stderr.writefln(" batch[%d]: %d targets, %d with recipe", i, batch.length, w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool builtSomething = false;
|
||||||
|
foreach (batch; batches)
|
||||||
|
{
|
||||||
|
foreach (ref t; batch)
|
||||||
|
{
|
||||||
|
if (!needsRebuild(t.name, t.prerequisites,
|
||||||
|
&graph.phonyTargets, &vpath, &t.orderOnlyPrereqs))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
builtSomething = true;
|
||||||
|
|
||||||
|
// Execute recipe lines
|
||||||
|
foreach (recipeLine; t.recipe)
|
||||||
|
{
|
||||||
|
// Expand variables in the recipe
|
||||||
|
string expanded = expand(recipeLine, env, t.name,
|
||||||
|
t.prerequisites);
|
||||||
|
|
||||||
|
// Print the command unless silent (@ prefix)
|
||||||
|
import std.string : stripLeft;
|
||||||
|
string trimmed = recipeLine.stripLeft();
|
||||||
|
if (config.dryRun || config.debugMode || recipeLine.length == 0 ||
|
||||||
|
(trimmed.length > 0 && trimmed[0] != '@'))
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build environment for recipe execution
|
||||||
|
// (propagate SHELL from Makefile if set)
|
||||||
|
string[] execEnv;
|
||||||
|
if (env.hasKey("SHELL"))
|
||||||
|
execEnv ~= "SHELL=" ~ env.get("SHELL");
|
||||||
|
|
||||||
|
// Serialize MAKEFLAGS for recursive $(MAKE) calls
|
||||||
|
import antelope.compatibility.submake;
|
||||||
|
string makeFlags = serializeMakeFlags(config);
|
||||||
|
if (makeFlags.length > 0)
|
||||||
|
execEnv ~= "MAKEFLAGS=" ~ makeFlags;
|
||||||
|
|
||||||
|
// Execute unless dry run
|
||||||
|
if (!config.dryRun)
|
||||||
|
{
|
||||||
|
auto result = execute(expanded, execEnv);
|
||||||
|
if (!result.success)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: *** [" ~ t.name ~
|
||||||
|
"] Error " ~ result.exitCode.to!string);
|
||||||
|
return result.exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!builtSomething)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "antelope: '" ~ targetName ~
|
||||||
|
"' is up to date.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find which build file to use based on mode and config.
|
||||||
|
private string findBuildFile(CliConfig config)
|
||||||
|
{
|
||||||
|
// Explicitly specified file
|
||||||
|
if (config.file.length > 0)
|
||||||
|
{
|
||||||
|
if (exists(config.file))
|
||||||
|
return config.file;
|
||||||
|
log(LogLevel.normal, "antelope: " ~ config.file ~ ": No such file");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// GNU Make mode: GNUmakefile, Makefile, makefile
|
||||||
|
if (config.gnuMode)
|
||||||
|
{
|
||||||
|
if (exists("GNUmakefile")) return "GNUmakefile";
|
||||||
|
if (exists("Makefile")) return "Makefile";
|
||||||
|
if (exists("makefile")) return "makefile";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native mode: antefile, antelope (case-insensitive)
|
||||||
|
if (exists("antefile")) return "antefile";
|
||||||
|
if (exists("Antefile")) return "Antefile";
|
||||||
|
if (exists("ANTEFILE")) return "ANTEFILE";
|
||||||
|
if (exists("antelope")) return "antelope";
|
||||||
|
if (exists("Antelope")) return "Antelope";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a Makefile to an Antefile (context-aware, late-stage feature).
|
||||||
|
int runHunt(CliConfig config)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "Antelope hunt — not yet implemented");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run autotools configure.ac replacement (future feature).
|
||||||
|
int runConfigure(CliConfig config)
|
||||||
|
{
|
||||||
|
log(LogLevel.normal, "Antelope configure — not yet implemented");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/// Version information.
|
||||||
|
module antelope.cli.verinfo;
|
||||||
|
|
||||||
|
/// Package version string.
|
||||||
|
immutable string versionString = "0.1.0";
|
||||||
|
|
||||||
|
/// Print version and exit.
|
||||||
|
void printVersion()
|
||||||
|
{
|
||||||
|
import std.stdio;
|
||||||
|
writeln("Antelope ", versionString);
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
/// GNU Make automatic variables: $@, $<, $^, $+, $*, $?, $%, $|, and their variants.
|
||||||
|
///
|
||||||
|
/// Automatic variables are set implicitly by GNU Make during rule execution.
|
||||||
|
/// This module defines them and controls when they are available, including
|
||||||
|
/// the directory/file-component variants ($(@D), $(@F), $(<D), $(<F), etc.).
|
||||||
|
module antelope.compatibility.automatic_vars;
|
||||||
|
|
||||||
|
/// All automatic variables GNU Make provides.
|
||||||
|
enum AutomaticVar
|
||||||
|
{
|
||||||
|
/// $@ — target name.
|
||||||
|
target,
|
||||||
|
/// $< — first prerequisite.
|
||||||
|
firstPrereq,
|
||||||
|
/// $^ — all prerequisites (deduplicated).
|
||||||
|
allPrereqs,
|
||||||
|
/// $+ — all prerequisites (duplicates preserved).
|
||||||
|
allPrereqsPlus,
|
||||||
|
/// $* — stem from pattern match.
|
||||||
|
stem,
|
||||||
|
/// $? — prerequisites newer than target.
|
||||||
|
newerPrereqs,
|
||||||
|
/// $% — archive member name.
|
||||||
|
archiveMember,
|
||||||
|
/// $| — order-only prerequisites.
|
||||||
|
orderOnlyPrereqs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Directory/file component variants for each automatic variable.
|
||||||
|
enum AutoVarComponent
|
||||||
|
{ dir, file, suffix, basename }
|
||||||
|
|
||||||
|
/// Evaluate an automatic variable, returning its string value.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// var = the automatic variable to resolve
|
||||||
|
/// target = the current target name ($@)
|
||||||
|
/// prereqs = the list of prerequisites
|
||||||
|
/// stem = the stem from pattern matching ($*)
|
||||||
|
///
|
||||||
|
/// Returns: the string value of the requested automatic variable.
|
||||||
|
string getAutomaticVar(AutomaticVar var, string target, string[] prereqs, string stem = "")
|
||||||
|
{
|
||||||
|
import std.algorithm.iteration : filter;
|
||||||
|
import std.array : array, join;
|
||||||
|
|
||||||
|
final switch (var)
|
||||||
|
{
|
||||||
|
case AutomaticVar.target:
|
||||||
|
return target;
|
||||||
|
|
||||||
|
case AutomaticVar.firstPrereq:
|
||||||
|
if (prereqs.length == 0)
|
||||||
|
return "";
|
||||||
|
return prereqs[0];
|
||||||
|
|
||||||
|
case AutomaticVar.allPrereqs:
|
||||||
|
{
|
||||||
|
// Space-joined, duplicates removed, order preserved.
|
||||||
|
bool[string] seen;
|
||||||
|
string[] unique;
|
||||||
|
foreach (p; prereqs)
|
||||||
|
{
|
||||||
|
if (p in seen)
|
||||||
|
continue;
|
||||||
|
seen[p] = true;
|
||||||
|
unique ~= p;
|
||||||
|
}
|
||||||
|
return unique.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
case AutomaticVar.allPrereqsPlus:
|
||||||
|
// Space-joined, duplicates preserved.
|
||||||
|
return prereqs.join(" ");
|
||||||
|
|
||||||
|
case AutomaticVar.stem:
|
||||||
|
return stem;
|
||||||
|
|
||||||
|
case AutomaticVar.newerPrereqs:
|
||||||
|
// Return all prereqs for now — timestamp filtering is delegated.
|
||||||
|
return prereqs.join(" ");
|
||||||
|
|
||||||
|
case AutomaticVar.archiveMember:
|
||||||
|
// Not yet implemented.
|
||||||
|
return "";
|
||||||
|
|
||||||
|
case AutomaticVar.orderOnlyPrereqs:
|
||||||
|
// Not yet implemented.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the directory part of a path.
|
||||||
|
/// "src/sub/file.o" → "src/sub/"
|
||||||
|
/// "file.o" → "./"
|
||||||
|
string dirPart(string path)
|
||||||
|
{
|
||||||
|
import std.string : lastIndexOf;
|
||||||
|
auto idx = path.lastIndexOf('/');
|
||||||
|
if (idx == -1) return "./";
|
||||||
|
return path[0 .. idx + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the file part (basename + suffix) of a path.
|
||||||
|
/// "src/sub/file.o" → "file.o"
|
||||||
|
/// "file.o" → "file.o"
|
||||||
|
string filePart(string path)
|
||||||
|
{
|
||||||
|
import std.string : lastIndexOf;
|
||||||
|
auto idx = path.lastIndexOf('/');
|
||||||
|
if (idx == -1) return path;
|
||||||
|
return path[idx + 1 .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the basename (filename without suffix) of a path.
|
||||||
|
/// "src/sub/file.o" → "file"
|
||||||
|
/// "src/sub/file" → "file"
|
||||||
|
/// ".hidden" → ".hidden"
|
||||||
|
string basePart(string path)
|
||||||
|
{
|
||||||
|
string f = filePart(path);
|
||||||
|
import std.string : lastIndexOf;
|
||||||
|
auto idx = f.lastIndexOf('.');
|
||||||
|
if (idx == -1 || idx == 0) return f;
|
||||||
|
return f[0 .. idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the suffix (extension including dot) of a path.
|
||||||
|
/// "src/sub/file.o" → ".o"
|
||||||
|
/// "src/sub/file" → ""
|
||||||
|
string suffixPart(string path)
|
||||||
|
{
|
||||||
|
string f = filePart(path);
|
||||||
|
import std.string : lastIndexOf;
|
||||||
|
auto idx = f.lastIndexOf('.');
|
||||||
|
if (idx == -1 || idx == 0) return "";
|
||||||
|
return f[idx .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a component from a path by AutoVarComponent.
|
||||||
|
/// This is used for the $(@D), $(@F), $(<D), $(<F), etc. variants.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// path = the resolved automatic variable value
|
||||||
|
/// comp = which component to extract
|
||||||
|
///
|
||||||
|
/// Returns: the extracted component string.
|
||||||
|
string extractComponent(string path, AutoVarComponent comp)
|
||||||
|
{
|
||||||
|
final switch (comp)
|
||||||
|
{
|
||||||
|
case AutoVarComponent.dir:
|
||||||
|
return dirPart(path);
|
||||||
|
case AutoVarComponent.file:
|
||||||
|
return filePart(path);
|
||||||
|
case AutoVarComponent.suffix:
|
||||||
|
return suffixPart(path);
|
||||||
|
case AutoVarComponent.basename:
|
||||||
|
return basePart(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unittests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// --- getAutomaticVar ---
|
||||||
|
|
||||||
|
// target
|
||||||
|
assert(getAutomaticVar(AutomaticVar.target, "foo.o", []) == "foo.o");
|
||||||
|
|
||||||
|
// firstPrereq
|
||||||
|
assert(getAutomaticVar(AutomaticVar.firstPrereq, "t", []) == "");
|
||||||
|
assert(getAutomaticVar(AutomaticVar.firstPrereq, "t", ["a.c"]) == "a.c");
|
||||||
|
assert(getAutomaticVar(AutomaticVar.firstPrereq, "t", ["a.c", "b.c"]) == "a.c");
|
||||||
|
|
||||||
|
// allPrereqs ($^) — deduplicated
|
||||||
|
assert(getAutomaticVar(AutomaticVar.allPrereqs, "t", ["a.c", "b.c", "a.c"]) == "a.c b.c");
|
||||||
|
assert(getAutomaticVar(AutomaticVar.allPrereqs, "t", []) == "");
|
||||||
|
|
||||||
|
// allPrereqsPlus ($+) — duplicates preserved
|
||||||
|
assert(getAutomaticVar(AutomaticVar.allPrereqsPlus, "t", ["a.c", "b.c", "a.c"]) == "a.c b.c a.c");
|
||||||
|
assert(getAutomaticVar(AutomaticVar.allPrereqsPlus, "t", []) == "");
|
||||||
|
|
||||||
|
// stem
|
||||||
|
assert(getAutomaticVar(AutomaticVar.stem, "t", [], "") == "");
|
||||||
|
assert(getAutomaticVar(AutomaticVar.stem, "t", [], "build/foo") == "build/foo");
|
||||||
|
|
||||||
|
// newerPrereqs
|
||||||
|
assert(getAutomaticVar(AutomaticVar.newerPrereqs, "t", ["a.c", "b.c"]) == "a.c b.c");
|
||||||
|
assert(getAutomaticVar(AutomaticVar.newerPrereqs, "t", []) == "");
|
||||||
|
|
||||||
|
// archiveMember (stub)
|
||||||
|
assert(getAutomaticVar(AutomaticVar.archiveMember, "t", []) == "");
|
||||||
|
|
||||||
|
// orderOnlyPrereqs (stub)
|
||||||
|
assert(getAutomaticVar(AutomaticVar.orderOnlyPrereqs, "t", []) == "");
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// --- Component extraction helpers ---
|
||||||
|
|
||||||
|
// dirPart
|
||||||
|
assert(dirPart("src/sub/file.o") == "src/sub/");
|
||||||
|
assert(dirPart("file.o") == "./");
|
||||||
|
assert(dirPart("/absolute/path/file") == "/absolute/path/");
|
||||||
|
assert(dirPart("") == "./"); // edge: empty path
|
||||||
|
|
||||||
|
// filePart
|
||||||
|
assert(filePart("src/sub/file.o") == "file.o");
|
||||||
|
assert(filePart("file.o") == "file.o");
|
||||||
|
assert(filePart("/a/b/c") == "c");
|
||||||
|
assert(filePart("") == ""); // edge: empty path
|
||||||
|
|
||||||
|
// basePart
|
||||||
|
assert(basePart("src/sub/file.o") == "file");
|
||||||
|
assert(basePart("file.o") == "file");
|
||||||
|
assert(basePart("file") == "file");
|
||||||
|
assert(basePart(".hidden") == ".hidden"); // dotfiles: no suffix
|
||||||
|
assert(basePart("src/sub/file") == "file");
|
||||||
|
|
||||||
|
// suffixPart
|
||||||
|
assert(suffixPart("src/sub/file.o") == ".o");
|
||||||
|
assert(suffixPart("file.o") == ".o");
|
||||||
|
assert(suffixPart("file") == "");
|
||||||
|
assert(suffixPart(".hidden") == ""); // dotfiles: not a suffix
|
||||||
|
assert(suffixPart("src/sub/file") == "");
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// --- extractComponent ---
|
||||||
|
|
||||||
|
string p = "src/sub/file.o";
|
||||||
|
assert(extractComponent(p, AutoVarComponent.dir) == "src/sub/");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.file) == "file.o");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.suffix) == ".o");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.basename) == "file");
|
||||||
|
|
||||||
|
// dotfile edge: ".hidden" treated as basename with no suffix
|
||||||
|
p = ".hidden";
|
||||||
|
assert(extractComponent(p, AutoVarComponent.dir) == "./");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.file) == ".hidden");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.suffix) == "");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.basename) == ".hidden");
|
||||||
|
|
||||||
|
// no-suffix path
|
||||||
|
p = "justdir/";
|
||||||
|
// note: trailing slash means filePart is "" after stripping
|
||||||
|
assert(extractComponent(p, AutoVarComponent.file) == "");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.suffix) == "");
|
||||||
|
assert(extractComponent(p, AutoVarComponent.basename) == "");
|
||||||
|
|
||||||
|
// component extraction on automatic vars: $(@D) where target is dir/file.o
|
||||||
|
string target = "build/foo.o";
|
||||||
|
assert(extractComponent(target, AutoVarComponent.dir) == "build/");
|
||||||
|
assert(extractComponent(target, AutoVarComponent.file) == "foo.o");
|
||||||
|
assert(extractComponent(target, AutoVarComponent.basename) == "foo");
|
||||||
|
assert(extractComponent(target, AutoVarComponent.suffix) == ".o");
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/// GNU Make syntax and semantics compatibility layer.
|
||||||
|
module antelope.compatibility.gnu_make;
|
||||||
|
|
||||||
|
/// Feature flags for GNU Make version targeting.
|
||||||
|
enum GnuMakeVersion
|
||||||
|
{
|
||||||
|
v3_81,
|
||||||
|
v4_0,
|
||||||
|
v4_1,
|
||||||
|
v4_2,
|
||||||
|
v4_3,
|
||||||
|
v4_4,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure which GNU Make features to emulate.
|
||||||
|
struct GnuMakeCompat
|
||||||
|
{
|
||||||
|
GnuMakeVersion targetVersion = GnuMakeVersion.v4_4;
|
||||||
|
bool enableSecondaryExpansion;
|
||||||
|
bool enableGnuBuiltins;
|
||||||
|
bool enableGnuExtensions;
|
||||||
|
|
||||||
|
/// Returns a GnuMakeCompat configured for full GNU Make 4.4 compatibility.
|
||||||
|
static GnuMakeCompat withDefaults()
|
||||||
|
{
|
||||||
|
return GnuMakeCompat(GnuMakeVersion.v4_4, true, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/// GNU Make's built-in implicit rule database (suffix rules + pattern rules).
|
||||||
|
///
|
||||||
|
/// GNU Make ships a large set of built-in rules for common file transformations
|
||||||
|
/// (.c → .o, .c → .exe, etc.), defined as suffix rules and pattern rules.
|
||||||
|
/// This module reproduces that database and provides the matching algorithm.
|
||||||
|
module antelope.compatibility.implicit_rules;
|
||||||
|
|
||||||
|
import std.string : indexOf;
|
||||||
|
|
||||||
|
/// A single built-in implicit rule.
|
||||||
|
struct ImplicitRule
|
||||||
|
{
|
||||||
|
string description;
|
||||||
|
string[2] suffixes; /// For suffix rules: .c → .o
|
||||||
|
string targetPattern; /// For pattern rules: %.o
|
||||||
|
string prereqPattern; /// For pattern rules: %.c
|
||||||
|
string[] recipe;
|
||||||
|
bool doubleSuffix; /// true = .c.o:, false = %.o: %.c
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the full set of GNU Make built-in rules.
|
||||||
|
ImplicitRule[] builtinRules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// --- C Compilation ---
|
||||||
|
ImplicitRule("Compile C source to object file",
|
||||||
|
["", ""], "%.o", "%.c",
|
||||||
|
["$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile C++ source (.cc) to object file",
|
||||||
|
["", ""], "%.o", "%.cc",
|
||||||
|
["$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile C++ source (.cpp) to object file",
|
||||||
|
["", ""], "%.o", "%.cpp",
|
||||||
|
["$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile C++ source (.C) to object file",
|
||||||
|
["", ""], "%.o", "%.C",
|
||||||
|
["$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile C++ source (.cxx) to object file",
|
||||||
|
["", ""], "%.o", "%.cxx",
|
||||||
|
["$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@"], false),
|
||||||
|
|
||||||
|
// --- Assembly ---
|
||||||
|
ImplicitRule("Assemble source to object file",
|
||||||
|
["", ""], "%.o", "%.s",
|
||||||
|
["$(AS) $(ASFLAGS) $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile preprocessed assembly to object file",
|
||||||
|
["", ""], "%.o", "%.S",
|
||||||
|
["$(CC) $(CPPFLAGS) -c $< -o $@"], false),
|
||||||
|
|
||||||
|
// --- Fortran ---
|
||||||
|
ImplicitRule("Compile Fortran (.f) to object file",
|
||||||
|
["", ""], "%.o", "%.f",
|
||||||
|
["$(FC) $(FFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile Fortran (.F) with preprocessing to object file",
|
||||||
|
["", ""], "%.o", "%.F",
|
||||||
|
["$(FC) $(FFLAGS) $(CPPFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile Fortran 90 (.f90) to object file",
|
||||||
|
["", ""], "%.o", "%.f90",
|
||||||
|
["$(FC) $(FFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile Fortran 95 (.f95) to object file",
|
||||||
|
["", ""], "%.o", "%.f95",
|
||||||
|
["$(FC) $(FFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile Fortran (.for) to object file",
|
||||||
|
["", ""], "%.o", "%.for",
|
||||||
|
["$(FC) $(FFLAGS) -c $< -o $@"], false),
|
||||||
|
|
||||||
|
// --- Modula-2, Pascal, Ratfor ---
|
||||||
|
ImplicitRule("Compile Modula-2 to object file",
|
||||||
|
["", ""], "%.o", "%.mod",
|
||||||
|
["$(M2C) $(M2FLAGS) $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile Pascal to object file",
|
||||||
|
["", ""], "%.o", "%.p",
|
||||||
|
["$(PC) $(PFLAGS) -c $< -o $@"], false),
|
||||||
|
ImplicitRule("Compile Ratfor to object file",
|
||||||
|
["", ""], "%.o", "%.r",
|
||||||
|
["$(FC) $(FFLAGS) $(RFLAGS) -c $< -o $@"], false),
|
||||||
|
|
||||||
|
// --- Lex / Yacc ---
|
||||||
|
ImplicitRule("Generate C source with Lex",
|
||||||
|
["", ""], "%.c", "%.l",
|
||||||
|
["$(LEX) $(LFLAGS) -t $< > $@"], false),
|
||||||
|
ImplicitRule("Generate C source with Yacc",
|
||||||
|
["", ""], "%.c", "%.y",
|
||||||
|
["$(YACC) $(YFLAGS) $< && mv y.tab.c $@"], false),
|
||||||
|
ImplicitRule("Generate C header with Yacc",
|
||||||
|
["", ""], "%.h", "%.y",
|
||||||
|
["$(YACC) $(YFLAGS) -d $< && mv y.tab.h $@"], false),
|
||||||
|
|
||||||
|
// --- Linker (C and C++) ---
|
||||||
|
ImplicitRule("Link C object file into executable",
|
||||||
|
["", ""], "%", "%.o",
|
||||||
|
["$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@"], false),
|
||||||
|
ImplicitRule("Link C++ object (.cc) into executable",
|
||||||
|
["", ""], "%", "%.cc",
|
||||||
|
["$(CXX) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@"], false),
|
||||||
|
ImplicitRule("Link C++ object (.cpp) into executable",
|
||||||
|
["", ""], "%", "%.cpp",
|
||||||
|
["$(CXX) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@"], false),
|
||||||
|
ImplicitRule("Link C++ object (.C) into executable",
|
||||||
|
["", ""], "%", "%.C",
|
||||||
|
["$(CXX) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@"], false),
|
||||||
|
ImplicitRule("Link Fortran object (.f) into executable",
|
||||||
|
["", ""], "%", "%.f",
|
||||||
|
["$(FC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@"], false),
|
||||||
|
ImplicitRule("Link Fortran object (.f90) into executable",
|
||||||
|
["", ""], "%", "%.f90",
|
||||||
|
["$(FC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@"], false),
|
||||||
|
|
||||||
|
// --- Preprocessing and assembly generation ---
|
||||||
|
ImplicitRule("Preprocess C source to output file",
|
||||||
|
["", ""], "%.i", "%.c",
|
||||||
|
["$(CC) $(CPPFLAGS) -E $< -o $@"], false),
|
||||||
|
ImplicitRule("Generate assembly from C source",
|
||||||
|
["", ""], "%.s", "%.c",
|
||||||
|
["$(CC) $(CPPFLAGS) $(CFLAGS) -S $< -o $@"], false),
|
||||||
|
ImplicitRule("Preprocess assembly source",
|
||||||
|
["", ""], "%.s", "%.S",
|
||||||
|
["$(CPP) $(CPPFLAGS) $< -o $@"], false),
|
||||||
|
|
||||||
|
// --- Texinfo / TeX ---
|
||||||
|
ImplicitRule("Generate DVI from Texinfo source",
|
||||||
|
["", ""], "%.dvi", "%.texinfo",
|
||||||
|
["$(TEXI2DVI) $(TEXI2DVI_FLAGS) $<"], false),
|
||||||
|
ImplicitRule("Generate DVI from TeX source",
|
||||||
|
["", ""], "%.dvi", "%.tex",
|
||||||
|
["$(TEX) $<"], false),
|
||||||
|
|
||||||
|
// --- CWEB ---
|
||||||
|
ImplicitRule("Generate C source from CWEB",
|
||||||
|
["", ""], "%.c", "%.w",
|
||||||
|
["$(CTANGLE) $< -o $@"], false),
|
||||||
|
ImplicitRule("Generate TeX source from CWEB",
|
||||||
|
["", ""], "%.tex", "%.w",
|
||||||
|
["$(CWEAVE) $< -o $@"], false),
|
||||||
|
|
||||||
|
// --- SCCS ---
|
||||||
|
ImplicitRule("Get source file from SCCS",
|
||||||
|
["", ""], "%", "s.%",
|
||||||
|
["$(GET) $(GFLAGS) $<"], false),
|
||||||
|
ImplicitRule("Get source file from SCCS subdirectory",
|
||||||
|
["", ""], "%", "SCCS/s.%",
|
||||||
|
["$(GET) $(GFLAGS) $<"], false),
|
||||||
|
|
||||||
|
// --- Archive ---
|
||||||
|
ImplicitRule("Update archive from object file",
|
||||||
|
["", ""], "%.a", "%.o",
|
||||||
|
["$(AR) $(ARFLAGS) $@ $^"], false),
|
||||||
|
|
||||||
|
// --- Suffix rules (legacy equivalents) ---
|
||||||
|
ImplicitRule("Suffix rule: .c to .o",
|
||||||
|
["c", "o"], "", "",
|
||||||
|
["$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@"], true),
|
||||||
|
ImplicitRule("Suffix rule: .cc to .o",
|
||||||
|
["cc", "o"], "", "",
|
||||||
|
["$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@"], true),
|
||||||
|
ImplicitRule("Suffix rule: .f to .o",
|
||||||
|
["f", "o"], "", "",
|
||||||
|
["$(FC) $(FFLAGS) -c $< -o $@"], true),
|
||||||
|
ImplicitRule("Suffix rule: .s to .o",
|
||||||
|
["s", "o"], "", "",
|
||||||
|
["$(AS) $(ASFLAGS) $< -o $@"], true),
|
||||||
|
ImplicitRule("Suffix rule: .y to .c",
|
||||||
|
["y", "c"], "", "",
|
||||||
|
["$(YACC) $(YFLAGS) $< && mv y.tab.c $@"], true),
|
||||||
|
ImplicitRule("Suffix rule: .l to .c",
|
||||||
|
["l", "c"], "", "",
|
||||||
|
["$(LEX) $(LFLAGS) -t $< > $@"], true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a successful implicit rule match.
|
||||||
|
struct ImplicitMatch
|
||||||
|
{
|
||||||
|
ImplicitRule rule;
|
||||||
|
string stem;
|
||||||
|
string resolvedTarget;
|
||||||
|
string resolvedPrereq;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to match an implicit rule for the given target.
|
||||||
|
///
|
||||||
|
/// Iterates built-in rules in order, trying each pattern against the target.
|
||||||
|
/// The first matching pattern rule wins (matching GNU Make semantics).
|
||||||
|
/// Returns null if no rule matches.
|
||||||
|
ImplicitMatch* matchImplicitRule(string target)
|
||||||
|
{
|
||||||
|
auto rules = builtinRules();
|
||||||
|
|
||||||
|
foreach (ref rule; rules)
|
||||||
|
{
|
||||||
|
string pattern = rule.targetPattern;
|
||||||
|
if (pattern.length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Split pattern on '%' — must have exactly one '%' (or two for edge cases)
|
||||||
|
size_t pct = pattern.indexOf('%');
|
||||||
|
if (pct == size_t.max)
|
||||||
|
{
|
||||||
|
// No wildcard — exact match
|
||||||
|
if (target == pattern)
|
||||||
|
return createMatch(rule, target, "", "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string prefix = pattern[0 .. pct];
|
||||||
|
string suffix = pattern[pct + 1 .. $];
|
||||||
|
|
||||||
|
// Target must start with prefix and end with suffix
|
||||||
|
if (target.length < prefix.length + suffix.length)
|
||||||
|
continue;
|
||||||
|
if (target[0 .. prefix.length] != prefix)
|
||||||
|
continue;
|
||||||
|
if (target[target.length - suffix.length .. $] != suffix)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Extract the stem (the part matching %)
|
||||||
|
string stem = target[prefix.length .. target.length - suffix.length];
|
||||||
|
|
||||||
|
// Resolve prerequisite by substituting stem into prereqPattern
|
||||||
|
string prereqPattern = rule.prereqPattern;
|
||||||
|
string resolvedPrereq = substitutePercent(prereqPattern, stem);
|
||||||
|
|
||||||
|
return createMatch(rule, target, stem, resolvedPrereq);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImplicitMatch* createMatch(ImplicitRule rule, string target, string stem, string resolvedPrereq)
|
||||||
|
{
|
||||||
|
auto match = new ImplicitMatch();
|
||||||
|
match.rule = rule;
|
||||||
|
match.stem = stem;
|
||||||
|
match.resolvedTarget = target;
|
||||||
|
match.resolvedPrereq = resolvedPrereq;
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Substitute the first '%' in pattern with replacement.
|
||||||
|
private string substitutePercent(string pattern, string replacement)
|
||||||
|
{
|
||||||
|
size_t pct = pattern.indexOf('%');
|
||||||
|
if (pct == size_t.max)
|
||||||
|
return pattern;
|
||||||
|
return pattern[0 .. pct] ~ replacement ~ pattern[pct + 1 .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: get the recipe lines from a match result.
|
||||||
|
string[] recipeLines(ImplicitMatch* match)
|
||||||
|
{
|
||||||
|
if (match is null)
|
||||||
|
return null;
|
||||||
|
return match.rule.recipe;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/// GNU Make include directive handling.
|
||||||
|
///
|
||||||
|
/// GNU Make supports three forms of the include directive:
|
||||||
|
/// include filename — error if missing
|
||||||
|
/// -include filename — warn (not error) if missing
|
||||||
|
/// sinclude filename — same as -include (POSIX compat)
|
||||||
|
///
|
||||||
|
/// Included files are read, parsed, and their rules merged into the current
|
||||||
|
/// makefile. If a missing included file can be rebuilt from an implicit rule,
|
||||||
|
/// GNU Make will rebuild it and restart.
|
||||||
|
module antelope.compatibility.include_handling;
|
||||||
|
|
||||||
|
/// How a missing include file is handled.
|
||||||
|
enum IncludeFailureMode
|
||||||
|
{
|
||||||
|
/// include — error out.
|
||||||
|
fatal,
|
||||||
|
/// -include / sinclude — warn and continue.
|
||||||
|
ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An include directive parsed from a Makefile.
|
||||||
|
struct IncludeDirective
|
||||||
|
{
|
||||||
|
string[] files;
|
||||||
|
IncludeFailureMode onFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to resolve an include file (VPATH, implicit rule, restart).
|
||||||
|
string resolveInclude(string filename)
|
||||||
|
{
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/// GNU Make order-only prerequisites (|) — must exist but don't trigger rebuilds.
|
||||||
|
///
|
||||||
|
/// GNU Make supports order-only prerequisites via the pipe (|) separator.
|
||||||
|
/// Prerequisites after the pipe must exist and be up-to-date before the
|
||||||
|
/// target is built, but they do NOT cause the target to be considered out
|
||||||
|
/// of date when they change.
|
||||||
|
///
|
||||||
|
/// Antelope rule syntax:
|
||||||
|
/// target: prereqs... | order-only-prereqs...
|
||||||
|
module antelope.compatibility.order_only;
|
||||||
|
|
||||||
|
/// A set of prerequisites split into normal and order-only.
|
||||||
|
struct PrereqSplit
|
||||||
|
{
|
||||||
|
string[] normal;
|
||||||
|
string[] orderOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a flat prerequisite list at the first | separator.
|
||||||
|
PrereqSplit splitPrereqs(string[] allPrereqs)
|
||||||
|
{
|
||||||
|
PrereqSplit result;
|
||||||
|
bool pastPipe;
|
||||||
|
foreach (p; allPrereqs)
|
||||||
|
{
|
||||||
|
if (p == "|")
|
||||||
|
pastPipe = true;
|
||||||
|
else if (pastPipe)
|
||||||
|
result.orderOnly ~= p;
|
||||||
|
else
|
||||||
|
result.normal ~= p;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/// GNU Make parallel execution semantics.
|
||||||
|
///
|
||||||
|
/// GNU Make's -j flag and related features control parallel execution:
|
||||||
|
/// - .NOTPARALLEL — disable parallelism for specific targets
|
||||||
|
/// - .WAIT — wait for previous prerequisites before continuing
|
||||||
|
/// - .JOBS — job pool (GNU Make 4.4+)
|
||||||
|
/// - jobserver protocol — pipe-based job token passing
|
||||||
|
///
|
||||||
|
/// This module defines the parallel execution model that Antelope uses
|
||||||
|
/// when compatibility with GNU Make's -j behavior is required.
|
||||||
|
module antelope.compatibility.parallel;
|
||||||
|
|
||||||
|
/// Parallel execution special targets.
|
||||||
|
enum ParallelSpecialTarget
|
||||||
|
{
|
||||||
|
notparallel, /// .NOTPARALLEL
|
||||||
|
wait, /// .WAIT
|
||||||
|
jobs, /// .JOBS
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parallel execution configuration.
|
||||||
|
struct ParallelConfig
|
||||||
|
{
|
||||||
|
/// Maximum parallel jobs (0 = unlimited, 1 = serial).
|
||||||
|
uint jobs = 1;
|
||||||
|
/// Targets excluded from parallel builds.
|
||||||
|
string[] notParallelTargets;
|
||||||
|
/// Whether to use jobserver protocol for sub-makes.
|
||||||
|
bool useJobserver = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/// GNU Make pattern rules and static pattern rules.
|
||||||
|
///
|
||||||
|
/// Pattern rules are GNU Make's primary mechanism for implicit and
|
||||||
|
/// static-pattern compilation rules. This module covers the matching
|
||||||
|
/// algorithm, stem extraction, order of precedence, and cancellation
|
||||||
|
/// (matching a rule with no recipe removes it).
|
||||||
|
module antelope.compatibility.pattern_rules;
|
||||||
|
|
||||||
|
import antelope.build.target;
|
||||||
|
import std.string : indexOf;
|
||||||
|
|
||||||
|
/// A pattern rule: %.o → %.c via recipe.
|
||||||
|
struct PatternRule
|
||||||
|
{
|
||||||
|
string targetPattern; /// e.g. "%.o"
|
||||||
|
string[] prereqPatterns; /// e.g. ["%.c"]
|
||||||
|
string[] recipe;
|
||||||
|
bool terminal; /// true = cancels inherited rules
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of matching a pattern rule against a target.
|
||||||
|
struct PatternMatch
|
||||||
|
{
|
||||||
|
PatternRule rule;
|
||||||
|
string stem; /// The matched wildcard portion.
|
||||||
|
string[] resolvedPrereqs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all pattern rules matching a target, ordered by GNU Make priority.
|
||||||
|
///
|
||||||
|
/// Checks against user-defined pattern rules stored in the list.
|
||||||
|
/// Pattern: "%.o" matches "foo.o", extracting stem "foo".
|
||||||
|
PatternMatch[] matchPatternRules(string target, PatternRule[] userRules)
|
||||||
|
{
|
||||||
|
PatternMatch[] matches;
|
||||||
|
foreach (ref rule; userRules)
|
||||||
|
{
|
||||||
|
if (rule.targetPattern.length == 0)
|
||||||
|
continue;
|
||||||
|
auto pct = indexOf(rule.targetPattern, '%');
|
||||||
|
if (pct < 0)
|
||||||
|
continue;
|
||||||
|
string prefix = rule.targetPattern[0 .. pct];
|
||||||
|
string suffix = rule.targetPattern[pct + 1 .. $];
|
||||||
|
|
||||||
|
// Must have at least one char for the stem
|
||||||
|
if (target.length < prefix.length + suffix.length + 1)
|
||||||
|
continue;
|
||||||
|
if (prefix.length > 0 && target[0 .. prefix.length] != prefix)
|
||||||
|
continue;
|
||||||
|
if (suffix.length > 0 && target[$ - suffix.length .. $] != suffix)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Extract stem and resolve prereqs
|
||||||
|
string stem = target[prefix.length .. target.length - suffix.length];
|
||||||
|
PatternMatch m;
|
||||||
|
m.rule = rule;
|
||||||
|
m.stem = stem;
|
||||||
|
m.resolvedPrereqs = [];
|
||||||
|
foreach (pp; rule.prereqPatterns)
|
||||||
|
{
|
||||||
|
auto pct2 = indexOf(pp, '%');
|
||||||
|
if (pct2 >= 0)
|
||||||
|
m.resolvedPrereqs ~= pp[0 .. pct2] ~ stem ~ pp[pct2 + 1 .. $];
|
||||||
|
else
|
||||||
|
m.resolvedPrereqs ~= pp;
|
||||||
|
}
|
||||||
|
matches ~= m;
|
||||||
|
if (rule.terminal)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a target name contains a % wildcard (i.e., is a pattern rule).
|
||||||
|
bool isPatternTarget(string name)
|
||||||
|
{
|
||||||
|
import std.string : indexOf;
|
||||||
|
return indexOf(name, '%') >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a PatternRule from a Target that was declared as a pattern rule
|
||||||
|
/// (target name contains '%').
|
||||||
|
PatternRule toPatternRule(Target t)
|
||||||
|
{
|
||||||
|
PatternRule r;
|
||||||
|
r.targetPattern = t.name;
|
||||||
|
r.prereqPatterns = t.prerequisites;
|
||||||
|
r.recipe = t.recipe;
|
||||||
|
r.terminal = false;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/// GNU Make's POSIX conformance mode (.POSIX target, -P flag).
|
||||||
|
///
|
||||||
|
/// GNU Make can run in POSIX-compatible mode, which disables certain GNU
|
||||||
|
/// extensions to conform to POSIX.1-2024. This module tracks which features
|
||||||
|
/// are affected and manages the conformance flags.
|
||||||
|
module antelope.compatibility.posix_make;
|
||||||
|
|
||||||
|
/// POSIX conformance level in GNU Make.
|
||||||
|
enum PosixConformance
|
||||||
|
{
|
||||||
|
/// Default GNU Make behavior — full extensions enabled.
|
||||||
|
gnu_mode,
|
||||||
|
/// .POSIX target declared — disable conflicting extensions.
|
||||||
|
posix_target,
|
||||||
|
/// -P / --posix flag — strict POSIX mode.
|
||||||
|
strict,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GNU Make features affected by POSIX conformance mode.
|
||||||
|
enum PosixAffectedFeature
|
||||||
|
{
|
||||||
|
/// $(shell ...) error handling differs.
|
||||||
|
shellErrorHandling,
|
||||||
|
/// The $$@ variable works differently in prerequisites.
|
||||||
|
automaticVarInPrereqs,
|
||||||
|
/// Order of pattern rule matching differs.
|
||||||
|
patternRuleOrder,
|
||||||
|
/// Archive member syntax behaviour.
|
||||||
|
archiveMembers,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track which features are restricted in the current conformance mode.
|
||||||
|
struct PosixCompat
|
||||||
|
{
|
||||||
|
PosixConformance mode = PosixConformance.gnu_mode;
|
||||||
|
PosixAffectedFeature[] restrictedFeatures;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/// Known GNU Make behavioral quirks and edge cases that Antelope must replicate.
|
||||||
|
///
|
||||||
|
/// GNU Make has accumulated decades of subtle behaviors that existing
|
||||||
|
/// Makefiles may depend on. This module catalogs them so Antelope can
|
||||||
|
/// emulate them when compatibility mode is active.
|
||||||
|
module antelope.compatibility.quirks;
|
||||||
|
|
||||||
|
/// A single GNU Make quirk Antelope can emulate.
|
||||||
|
struct GnuQuirk
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
string description;
|
||||||
|
uint introducedVersion;
|
||||||
|
bool enabledByDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All known GNU Make quirks.
|
||||||
|
GnuQuirk[] knownQuirks()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
GnuQuirk("Suspended line continuation in comments",
|
||||||
|
"Backslash-newline inside comments still joins lines.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Unescaped # in recipe lines",
|
||||||
|
"A # in a recipe line starts a comment, but only after expansion.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Variable assignment with trailing semicolon",
|
||||||
|
"foo=bar; is silently accepted as assignment.", 0, true),
|
||||||
|
GnuQuirk("Empty prerequisite list with pipe",
|
||||||
|
"target: | with no regular prereqs is allowed.", 0, true),
|
||||||
|
GnuQuirk("Automatic variable propagation",
|
||||||
|
"$(@D) and $(@F) work even when $@ is empty.", 0, true),
|
||||||
|
GnuQuirk("Recursive variable assignment on command line",
|
||||||
|
"Command-line overrides apply recursively.", 0, true),
|
||||||
|
GnuQuirk("Secondary expansion of .EXTRA_PREREQS",
|
||||||
|
".EXTRA_PREREQS undergoes secondary expansion.", 0, true),
|
||||||
|
|
||||||
|
GnuQuirk("Backslash-newline in variable values",
|
||||||
|
"A backslash followed by a newline inside a variable value is "
|
||||||
|
~ "consumed, joining the lines into one.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Trailing whitespace in = assignments",
|
||||||
|
"In a recursive variable assignment, trailing whitespace after "
|
||||||
|
~ "the value is preserved verbatim.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Empty recipe lines",
|
||||||
|
"A recipe line consisting of only a tab is a valid no-op.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Double-colon rules",
|
||||||
|
"A double-colon rule (target:: prereqs) allows multiple "
|
||||||
|
~ "independent recipes for the same target.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Pattern rule matching order",
|
||||||
|
"Match pattern rules in declaration order; the first matching "
|
||||||
|
~ "rule wins, including built-in rules.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("MAKEFLAGS propagation",
|
||||||
|
"MAKEFLAGS contains condensed flag letters (e.g. 'n', 'd') "
|
||||||
|
~ "and is automatically passed to sub-make invocations.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("export/unexport timing",
|
||||||
|
"The export directive takes effect immediately at parse time, "
|
||||||
|
~ "not deferred to recipe execution time.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Comment inside variable value after expansion",
|
||||||
|
"When a variable expands to a value containing #, that # "
|
||||||
|
~ "starts a comment in recipe context.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Nested include handling",
|
||||||
|
"Files included via include can themselves contain include "
|
||||||
|
~ "directives, recursively.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Target-specific variable override order",
|
||||||
|
"Override precedence: command-line > target-specific > "
|
||||||
|
~ "pattern-specific > global variable.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk(".ONESHELL behavior",
|
||||||
|
"With .ONESHELL, all recipe lines for a rule run in a single "
|
||||||
|
~ "shell invocation, not one per line.",
|
||||||
|
400, true),
|
||||||
|
GnuQuirk("Archive member syntax with ()",
|
||||||
|
"Archive members are referenced as libfoo.a(member.o) and "
|
||||||
|
~ "support implicit rules for extraction.",
|
||||||
|
0, true),
|
||||||
|
GnuQuirk("Automatic variables in prerequisite lists with .SECONDEXPANSION",
|
||||||
|
"Under secondary expansion, automatic variables ($@, $<, etc.) "
|
||||||
|
~ "are available in prerequisite lists.",
|
||||||
|
381, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/// GNU Make secondary expansion (.SECONDEXPANSION).
|
||||||
|
///
|
||||||
|
/// When .SECONDEXPANSION is declared as a target, GNU Make performs a
|
||||||
|
/// second expansion pass on the prerequisite list of all (or specified)
|
||||||
|
/// targets. This allows automatic variables ($@, $<, etc.) in prerequisite
|
||||||
|
/// lists, enabling patterns like:
|
||||||
|
///
|
||||||
|
/// .SECONDEXPANSION:
|
||||||
|
/// main.o: $$(patsubst %.c,%.o,$$@)
|
||||||
|
///
|
||||||
|
/// In the first pass, $$ becomes $; in the second pass, the result is
|
||||||
|
/// expanded with automatic variables set.
|
||||||
|
module antelope.compatibility.secondary_expansion;
|
||||||
|
|
||||||
|
/// Secondary expansion state.
|
||||||
|
struct SecondaryExpansion
|
||||||
|
{
|
||||||
|
/// Whether .SECONDEXPANSION is active.
|
||||||
|
bool enabled;
|
||||||
|
/// If non-empty, only these targets get secondary expansion.
|
||||||
|
string[] targetWhitelist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform secondary expansion on a prerequisite list.
|
||||||
|
///
|
||||||
|
/// When .SECONDEXPANSION is active, prerequisite lists undergo a second
|
||||||
|
/// expansion pass. In the first pass, `$$` becomes `$`. In the second pass,
|
||||||
|
/// the result is expanded with automatic variables set (since the target
|
||||||
|
/// is now known).
|
||||||
|
///
|
||||||
|
/// This function handles the second pass: each prerequisite string is
|
||||||
|
/// expanded with the current target context.
|
||||||
|
string[] expandSecondPass(string target, string[] prereqs,
|
||||||
|
void* expandFn = null, void* env = null)
|
||||||
|
{
|
||||||
|
// For now, just return prereqs as-is. Full implementation requires
|
||||||
|
// access to the expansion engine with automatic variable context.
|
||||||
|
// The expansion engine is called during recipe execution anyway.
|
||||||
|
return prereqs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if secondary expansion should be applied for a target.
|
||||||
|
/// Returns true if .SECONDEXPANSION is active and the target
|
||||||
|
/// is in the whitelist (or whitelist is empty = all targets).
|
||||||
|
bool shouldExpand(string target, SecondaryExpansion sec)
|
||||||
|
{
|
||||||
|
if (!sec.enabled)
|
||||||
|
return false;
|
||||||
|
if (sec.targetWhitelist.length == 0)
|
||||||
|
return true;
|
||||||
|
foreach (t; sec.targetWhitelist)
|
||||||
|
if (t == target) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/// GNU Make sub-make and recursive make communication.
|
||||||
|
///
|
||||||
|
/// GNU Make has specific protocols for communicating between parent and
|
||||||
|
/// child make processes: MAKEFLAGS, MAKE, variable export/unexport,
|
||||||
|
/// and the jobserver (--jobserver-style pipe).
|
||||||
|
///
|
||||||
|
/// This module handles:
|
||||||
|
/// - $(MAKE) / $(MAKECMDGOALS) propagation
|
||||||
|
/// - MAKEFLAGS / GNUMAKEFLAGS serialization
|
||||||
|
/// - Variable export to sub-makes
|
||||||
|
/// - Jobserver pipe inheritance (--jobserver-style)
|
||||||
|
module antelope.compatibility.submake;
|
||||||
|
|
||||||
|
import antelope.cli.args;
|
||||||
|
import std.conv : to;
|
||||||
|
import std.string : strip;
|
||||||
|
import std.string : strip;
|
||||||
|
|
||||||
|
/// Sub-make communication options.
|
||||||
|
struct SubMakeConfig
|
||||||
|
{
|
||||||
|
/// Serialize and pass MAKEFLAGS.
|
||||||
|
bool passMakeFlags = true;
|
||||||
|
/// Pass jobserver file descriptors.
|
||||||
|
bool passJobserver = true;
|
||||||
|
/// Export variables marked with `export`.
|
||||||
|
bool respectExportDirective = true;
|
||||||
|
/// Track recursion depth to avoid infinite loops.
|
||||||
|
uint maxRecursionDepth = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize MAKEFLAGS for a sub-make invocation.
|
||||||
|
///
|
||||||
|
/// Builds a space-separated string of GNU Make-compatible flags from the
|
||||||
|
/// current CLI configuration. The resulting string can be set as the
|
||||||
|
/// MAKEFLAGS environment variable for recursive $(MAKE) invocations.
|
||||||
|
///
|
||||||
|
/// Serialized flags:
|
||||||
|
/// -j<N> — parallel job count (only when jobs > 1)
|
||||||
|
/// -n — dry run
|
||||||
|
/// -P — POSIX conformance mode
|
||||||
|
/// -d — debug output
|
||||||
|
///
|
||||||
|
/// Returns: a space-delimited MAKEFLAGS string, or "" if no flags are active.
|
||||||
|
string serializeMakeFlags(CliConfig config)
|
||||||
|
{
|
||||||
|
string flags;
|
||||||
|
if (config.jobs > 1)
|
||||||
|
flags ~= " -j" ~ config.jobs.to!string;
|
||||||
|
if (config.dryRun)
|
||||||
|
flags ~= " -n";
|
||||||
|
if (config.posix)
|
||||||
|
flags ~= " -P";
|
||||||
|
if (config.debugMode)
|
||||||
|
flags ~= " -d";
|
||||||
|
return flags.strip;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/// GNU Make target-specific and pattern-specific variable assignments.
|
||||||
|
///
|
||||||
|
/// GNU Make allows variable values to be scoped to specific targets or
|
||||||
|
/// patterns, overriding global values only when building that target:
|
||||||
|
/// target: VAR = value
|
||||||
|
/// target: VAR := value
|
||||||
|
/// target: VAR ::= value
|
||||||
|
/// target: VAR += value
|
||||||
|
/// target: VAR ?= value
|
||||||
|
/// %: VAR = value (pattern-specific)
|
||||||
|
module antelope.compatibility.target_vars;
|
||||||
|
|
||||||
|
/// The scope kind of a variable override.
|
||||||
|
enum TargetVarScope
|
||||||
|
{
|
||||||
|
/// target: VAR = value
|
||||||
|
targetSpecific,
|
||||||
|
/// pattern: VAR = value
|
||||||
|
patternSpecific,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A target-specific or pattern-specific variable assignment.
|
||||||
|
struct ScopedVariable
|
||||||
|
{
|
||||||
|
string targetPattern;
|
||||||
|
string name;
|
||||||
|
string value;
|
||||||
|
bool recursive; /// true for =, false for :=
|
||||||
|
TargetVarScope varScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a variable respecting target-scoped overrides.
|
||||||
|
string lookupScopedVar(string target, string varName)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/// GNU Make VPATH / vpath directive — directory search path for prerequisites.
|
||||||
|
///
|
||||||
|
/// GNU Make supports two mechanisms for searching directories:
|
||||||
|
/// - VPATH variable: search path for all prerequisites
|
||||||
|
/// - vpath directive: search path scoped by pattern (e.g. vpath %.h src/)
|
||||||
|
///
|
||||||
|
/// This module handles both the simple variable-based and the pattern-scoped
|
||||||
|
/// search, plus the interaction with implicit rule chaining.
|
||||||
|
module antelope.compatibility.vpath;
|
||||||
|
|
||||||
|
/// A single vpath pattern → directory mapping.
|
||||||
|
struct VPathEntry
|
||||||
|
{
|
||||||
|
string pattern; /// e.g. "%.h" — empty for VPATH variable entries.
|
||||||
|
string[] directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete VPATH configuration.
|
||||||
|
struct VPathConfig
|
||||||
|
{
|
||||||
|
/// From VPATH = dir1:dir2
|
||||||
|
string[] globalSearchDirs;
|
||||||
|
/// From vpath %.h src/
|
||||||
|
VPathEntry[] patternEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for a file across all VPATH directories.
|
||||||
|
///
|
||||||
|
/// Resolution order (matching GNU Make):
|
||||||
|
/// 1. Check if file exists locally → return as-is
|
||||||
|
/// 2. Check pattern-specific vpath entries (if filename matches pattern)
|
||||||
|
/// 3. Check global VPATH search dirs
|
||||||
|
/// 4. Return original filename if not found anywhere
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// filename = The prerequisite file to search for
|
||||||
|
/// config = VPATH configuration (global dirs + pattern entries)
|
||||||
|
///
|
||||||
|
/// Returns: The resolved path if found in a search directory, or the
|
||||||
|
/// original filename if not found anywhere.
|
||||||
|
string vpathResolve(string filename, const(VPathConfig) config)
|
||||||
|
{
|
||||||
|
import std.file : exists;
|
||||||
|
import std.path : buildPath;
|
||||||
|
|
||||||
|
// Local file exists → use it directly
|
||||||
|
if (exists(filename))
|
||||||
|
return filename;
|
||||||
|
|
||||||
|
// Check pattern-specific entries
|
||||||
|
foreach (entry; config.patternEntries)
|
||||||
|
{
|
||||||
|
if (entry.pattern.length == 0)
|
||||||
|
continue;
|
||||||
|
// Simple pattern matching: if pattern is "%.h", check if filename
|
||||||
|
// ends with ".h" (after the %)
|
||||||
|
if (patternMatches(filename, entry.pattern))
|
||||||
|
{
|
||||||
|
foreach (dir; entry.directories)
|
||||||
|
{
|
||||||
|
string candidate = buildPath(dir, filename);
|
||||||
|
if (exists(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check global search dirs
|
||||||
|
foreach (dir; config.globalSearchDirs)
|
||||||
|
{
|
||||||
|
if (dir.length == 0)
|
||||||
|
continue;
|
||||||
|
string candidate = buildPath(dir, filename);
|
||||||
|
if (exists(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found — return original
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple pattern match: "%.h" matches "foo.h", "bar/baz.h", etc.
|
||||||
|
/// Splits pattern on '%' into prefix and suffix; checks if filename
|
||||||
|
/// starts with prefix and ends with suffix, with at least one char in between.
|
||||||
|
private bool patternMatches(string filename, string pattern)
|
||||||
|
{
|
||||||
|
import std.string : indexOf;
|
||||||
|
auto pct = indexOf(pattern, '%');
|
||||||
|
if (pct < 0)
|
||||||
|
return filename == pattern;
|
||||||
|
|
||||||
|
string prefix = pattern[0 .. pct];
|
||||||
|
string suffix = pattern[pct + 1 .. $];
|
||||||
|
|
||||||
|
// Must be at least one char between prefix and suffix
|
||||||
|
if (filename.length < prefix.length + suffix.length + 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (prefix.length > 0 && filename[0 .. prefix.length] != prefix)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (suffix.length > 0 && filename[$ - suffix.length .. $] != suffix)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/// Error types and structured error reporting.
|
||||||
|
module antelope.diagnostics.errors;
|
||||||
|
|
||||||
|
/// Categories of errors Antelope can produce.
|
||||||
|
enum ErrorKind
|
||||||
|
{
|
||||||
|
parseError,
|
||||||
|
undefinedVariable,
|
||||||
|
cyclicDependency,
|
||||||
|
missingTarget,
|
||||||
|
commandFailed,
|
||||||
|
fileNotFound,
|
||||||
|
internalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structured error with location information.
|
||||||
|
struct AntelopeError
|
||||||
|
{
|
||||||
|
ErrorKind kind;
|
||||||
|
string message;
|
||||||
|
string file;
|
||||||
|
size_t line;
|
||||||
|
size_t column;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/// Output formatting — colored terminal output, logging levels, and verbosity.
|
||||||
|
module antelope.diagnostics.output;
|
||||||
|
|
||||||
|
import std.stdio;
|
||||||
|
import core.sys.posix.unistd : isatty;
|
||||||
|
|
||||||
|
/// Log verbosity level.
|
||||||
|
enum LogLevel
|
||||||
|
{
|
||||||
|
quiet,
|
||||||
|
normal,
|
||||||
|
verbose,
|
||||||
|
dbg,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current global log level threshold.
|
||||||
|
/// Messages below this level are suppressed.
|
||||||
|
LogLevel currentLogLevel = LogLevel.normal;
|
||||||
|
|
||||||
|
/// Set the current log level threshold.
|
||||||
|
/// Params: level = new verbosity threshold.
|
||||||
|
void setLogLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
currentLogLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a log message at the given level.
|
||||||
|
///
|
||||||
|
/// Filters by `currentLogLevel`: messages below the threshold are suppressed.
|
||||||
|
/// When stdout is a TTY, the output is colored:
|
||||||
|
/// - `normal` → bold
|
||||||
|
/// - `verbose` → plain
|
||||||
|
/// - `dbg` → dim/grey
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// level = severity of this message
|
||||||
|
/// message = the text to write
|
||||||
|
void log(LogLevel level, string message)
|
||||||
|
{
|
||||||
|
// Quiet mode suppresses everything.
|
||||||
|
if (currentLogLevel == LogLevel.quiet)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Suppress messages below the threshold (unless in debug mode).
|
||||||
|
if (currentLogLevel != LogLevel.dbg && level < currentLogLevel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Plain output when not a terminal.
|
||||||
|
if (isatty(stdout.fileno) == 0)
|
||||||
|
{
|
||||||
|
writeln(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colored output on TTY.
|
||||||
|
final switch (level)
|
||||||
|
{
|
||||||
|
case LogLevel.quiet:
|
||||||
|
// Quiet messages are filtered above; this is unreachable.
|
||||||
|
return;
|
||||||
|
case LogLevel.normal:
|
||||||
|
writefln("\x1b[1m%s\x1b[22m", message);
|
||||||
|
break;
|
||||||
|
case LogLevel.verbose:
|
||||||
|
writeln(message);
|
||||||
|
break;
|
||||||
|
case LogLevel.dbg:
|
||||||
|
writefln("\x1b[2m%s\x1b[22m", message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
import std.exception : assertNotThrown;
|
||||||
|
|
||||||
|
// Default level is normal.
|
||||||
|
assert(currentLogLevel == LogLevel.normal);
|
||||||
|
|
||||||
|
// setLogLevel round-trip.
|
||||||
|
setLogLevel(LogLevel.quiet);
|
||||||
|
assert(currentLogLevel == LogLevel.quiet);
|
||||||
|
setLogLevel(LogLevel.normal);
|
||||||
|
assert(currentLogLevel == LogLevel.normal);
|
||||||
|
|
||||||
|
// All log levels can be called without throwing.
|
||||||
|
assertNotThrown!Exception(log(LogLevel.normal, "normal test message"));
|
||||||
|
assertNotThrown!Exception(log(LogLevel.verbose, "verbose test message"));
|
||||||
|
assertNotThrown!Exception(log(LogLevel.dbg, "dbg test message"));
|
||||||
|
|
||||||
|
// Quiet suppresses everything — no crash, no output.
|
||||||
|
setLogLevel(LogLevel.quiet);
|
||||||
|
assertNotThrown!Exception(log(LogLevel.normal, "should be suppressed"));
|
||||||
|
assertNotThrown!Exception(log(LogLevel.dbg, "should be suppressed"));
|
||||||
|
|
||||||
|
// Restore default for subsequent tests.
|
||||||
|
setLogLevel(LogLevel.normal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/// Warning diagnostics for non-fatal issues.
|
||||||
|
module antelope.diagnostics.warnings;
|
||||||
|
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
|
/// Categories of warnings.
|
||||||
|
enum WarningKind
|
||||||
|
{
|
||||||
|
deprecatedFeature,
|
||||||
|
undefinedVariable,
|
||||||
|
orderOnlyCircular,
|
||||||
|
phonyPrerequisite,
|
||||||
|
overrideConflict,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether warnings are currently enabled. Default: true.
|
||||||
|
__gshared bool warningsEnabled = true;
|
||||||
|
|
||||||
|
/// Enable or disable warnings globally.
|
||||||
|
/// Params: enabled = set to false to suppress all warning output.
|
||||||
|
void setWarningsEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
warningsEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a WarningKind to a human-readable string.
|
||||||
|
private string warningKindString(WarningKind kind)
|
||||||
|
{
|
||||||
|
final switch (kind)
|
||||||
|
{
|
||||||
|
case WarningKind.deprecatedFeature: return "deprecated-feature";
|
||||||
|
case WarningKind.undefinedVariable: return "undefined-variable";
|
||||||
|
case WarningKind.orderOnlyCircular: return "order-only-circular";
|
||||||
|
case WarningKind.phonyPrerequisite: return "phony-prerequisite";
|
||||||
|
case WarningKind.overrideConflict: return "override-conflict";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issue a warning with location.
|
||||||
|
/// Writes to stderr in the format: antelope: warning: <kind> <message> at <file>:<line>
|
||||||
|
/// Params:
|
||||||
|
/// kind = the category of warning
|
||||||
|
/// message = descriptive message for the user
|
||||||
|
/// file = source file where the warning originates (default: __FILE__)
|
||||||
|
/// line = source line where the warning originates (default: __LINE__)
|
||||||
|
void warn(WarningKind kind, string message, string file = __FILE__, size_t line = __LINE__)
|
||||||
|
{
|
||||||
|
if (!warningsEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
stderr.writeln("antelope: warning: ", warningKindString(kind),
|
||||||
|
" ", message, " at ", file, ":", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// Verify warningsEnabled guard works correctly.
|
||||||
|
auto oldEnabled = warningsEnabled;
|
||||||
|
|
||||||
|
// With warnings disabled, nothing should happen — just verify no crash.
|
||||||
|
warningsEnabled = false;
|
||||||
|
warn(WarningKind.deprecatedFeature, "should not appear", "test.d", 1);
|
||||||
|
|
||||||
|
// Re-enable and verify a basic call doesn't throw.
|
||||||
|
warningsEnabled = true;
|
||||||
|
warn(WarningKind.deprecatedFeature, "test warning", "test.d", 42);
|
||||||
|
warn(WarningKind.undefinedVariable, "undefined var", "foo.d", 10);
|
||||||
|
warn(WarningKind.orderOnlyCircular, "circular dep", "bar.d", 5);
|
||||||
|
warn(WarningKind.phonyPrerequisite, "phony prereq", "baz.d", 7);
|
||||||
|
warn(WarningKind.overrideConflict, "override conflict", "qux.d", 3);
|
||||||
|
|
||||||
|
// Restore original state.
|
||||||
|
warningsEnabled = oldEnabled;
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/// Conditional evaluation (ifeq/ifneq/ifdef/ifndef).
|
||||||
|
///
|
||||||
|
/// Evaluates GNU Make conditional expressions at parse/evaluate time.
|
||||||
|
/// Supports three syntactic forms:
|
||||||
|
/// - Parenthesized: `ifeq (a, b)`
|
||||||
|
/// - Quoted: `ifeq "a" "b"`
|
||||||
|
/// - Bare: `ifdef VAR_NAME`
|
||||||
|
module antelope.evaluator.conditionals;
|
||||||
|
|
||||||
|
import antelope.shell.environment;
|
||||||
|
import std.string : strip, indexOf;
|
||||||
|
import std.ascii : isWhite;
|
||||||
|
|
||||||
|
/// Evaluate a GNU Make conditional expression.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// op = conditional operator: "ifeq", "ifneq", "ifdef", or "ifndef"
|
||||||
|
/// lhs = left-hand argument (for paren form `ifeq (a, b)`,
|
||||||
|
/// lhs contains `(a, b)` and rhs is empty)
|
||||||
|
/// rhs = right-hand argument (empty for ifdef/ifndef and paren form)
|
||||||
|
/// env = optional Environment pointer for ifdef/ifndef variable lookups
|
||||||
|
///
|
||||||
|
/// Returns: true if the condition holds, false otherwise.
|
||||||
|
bool evaluateConditional(string op, string lhs, string rhs,
|
||||||
|
Environment* env = null)
|
||||||
|
{
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case "ifeq":
|
||||||
|
{
|
||||||
|
string a, b;
|
||||||
|
splitArgs(lhs, rhs, a, b);
|
||||||
|
// Expand variable references before comparison (GNU Make behavior)
|
||||||
|
if (env !is null)
|
||||||
|
{
|
||||||
|
import antelope.evaluator.expansion;
|
||||||
|
a = expand(a, env);
|
||||||
|
b = expand(b, env);
|
||||||
|
}
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ifneq":
|
||||||
|
{
|
||||||
|
string a, b;
|
||||||
|
splitArgs(lhs, rhs, a, b);
|
||||||
|
if (env !is null)
|
||||||
|
{
|
||||||
|
import antelope.evaluator.expansion;
|
||||||
|
a = expand(a, env);
|
||||||
|
b = expand(b, env);
|
||||||
|
}
|
||||||
|
return a != b;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ifdef":
|
||||||
|
{
|
||||||
|
auto key = stripQuotes(lhs.strip);
|
||||||
|
return (env !is null) && env.hasKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ifndef":
|
||||||
|
{
|
||||||
|
auto key = stripQuotes(lhs.strip);
|
||||||
|
return (env is null) || !env.hasKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split conditional arguments handling both parenthesized and
|
||||||
|
/// two-argument forms.
|
||||||
|
///
|
||||||
|
/// **Parenthesized form:** `ifeq (a, b)`
|
||||||
|
/// - `lhs` = `(a, b)`, `rhs` = `""`
|
||||||
|
/// - Strips parens, splits on the first comma, strips whitespace
|
||||||
|
/// and surrounding quotes from each part.
|
||||||
|
///
|
||||||
|
/// **Two-argument form:** `ifeq "a" "b"` or `ifeq a b`
|
||||||
|
/// - Both `lhs` and `rhs` are provided.
|
||||||
|
/// - Strips whitespace and surrounding quotes from each.
|
||||||
|
private void splitArgs(string lhs, string rhs, out string a,
|
||||||
|
out string b)
|
||||||
|
{
|
||||||
|
// Paren form: ifeq (a, b)
|
||||||
|
if (lhs.length > 0 && lhs[0] == '(')
|
||||||
|
{
|
||||||
|
auto inner = lhs[1 .. $].strip;
|
||||||
|
// Strip closing paren
|
||||||
|
if (inner.length > 0 && inner[$ - 1] == ')')
|
||||||
|
inner = inner[0 .. $ - 1].strip;
|
||||||
|
|
||||||
|
auto comma = indexOf(inner, ',');
|
||||||
|
if (comma >= 0)
|
||||||
|
{
|
||||||
|
a = stripQuotes(inner[0 .. comma].strip);
|
||||||
|
b = stripQuotes(inner[comma + 1 .. $].strip);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No comma — entire inner is a (malformed but handle gracefully)
|
||||||
|
a = stripQuotes(inner.strip);
|
||||||
|
b = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Two-argument form: ifeq "a" "b" or ifeq a b
|
||||||
|
else
|
||||||
|
{
|
||||||
|
a = stripQuotes(lhs.strip);
|
||||||
|
b = stripQuotes(rhs.strip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip matching quote characters (" or ') from both ends of a string.
|
||||||
|
private string stripQuotes(string s)
|
||||||
|
{
|
||||||
|
if (s.length >= 2)
|
||||||
|
{
|
||||||
|
if ((s[0] == '"' && s[$ - 1] == '"')
|
||||||
|
|| (s[0] == '\'' && s[$ - 1] == '\''))
|
||||||
|
return s[1 .. $ - 1];
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unittests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
import std.stdio : writeln;
|
||||||
|
|
||||||
|
// --- ifeq simple ---
|
||||||
|
assert(evaluateConditional("ifeq", "a", "a"),
|
||||||
|
"ifeq: 'a' == 'a' should be true");
|
||||||
|
assert(!evaluateConditional("ifeq", "a", "b"),
|
||||||
|
"ifeq: 'a' == 'b' should be false");
|
||||||
|
|
||||||
|
// --- ifeq with double quotes ---
|
||||||
|
assert(evaluateConditional("ifeq", `"a"`, `"a"`),
|
||||||
|
"ifeq: '\"a\"' == '\"a\"' should be true");
|
||||||
|
assert(!evaluateConditional("ifeq", `"a"`, `"b"`),
|
||||||
|
"ifeq: '\"a\"' == '\"b\"' should be false");
|
||||||
|
|
||||||
|
// --- ifeq with single quotes ---
|
||||||
|
assert(evaluateConditional("ifeq", "'a'", "'a'"),
|
||||||
|
"ifeq: single-quoted equal should be true");
|
||||||
|
|
||||||
|
// --- ifneq ---
|
||||||
|
assert(evaluateConditional("ifneq", "a", "b"),
|
||||||
|
"ifneq: 'a' != 'b' should be true");
|
||||||
|
assert(!evaluateConditional("ifneq", "a", "a"),
|
||||||
|
"ifneq: 'a' != 'a' should be false");
|
||||||
|
|
||||||
|
// --- Paren form: ifeq (a, b) ---
|
||||||
|
assert(evaluateConditional("ifeq", "(a, a)", ""),
|
||||||
|
"ifeq paren form: (a, a) should be true");
|
||||||
|
assert(!evaluateConditional("ifeq", "(a, b)", ""),
|
||||||
|
"ifeq paren form: (a, b) should be false");
|
||||||
|
|
||||||
|
// --- Paren form with whitespace ---
|
||||||
|
assert(evaluateConditional("ifeq", "( a , a )", ""),
|
||||||
|
"ifeq paren form with whitespace should be true");
|
||||||
|
|
||||||
|
// --- Quoted form with whitespace ---
|
||||||
|
assert(evaluateConditional("ifeq", `" a "`, `" a "`),
|
||||||
|
"ifeq quoted with internal whitespace should be true");
|
||||||
|
|
||||||
|
// --- ifdef / ifndef with Environment ---
|
||||||
|
Environment env;
|
||||||
|
env.set("EXISTING_VAR", "value");
|
||||||
|
env.set("ANOTHER_VAR", "42");
|
||||||
|
|
||||||
|
assert(evaluateConditional("ifdef", "EXISTING_VAR", "", &env),
|
||||||
|
"ifdef: EXISTING_VAR exists should be true");
|
||||||
|
assert(!evaluateConditional("ifdef", "NONEXISTENT_VAR", "", &env),
|
||||||
|
"ifdef: NONEXISTENT_VAR missing should be false");
|
||||||
|
assert(!evaluateConditional("ifndef", "EXISTING_VAR", "", &env),
|
||||||
|
"ifndef: EXISTING_VAR exists should be false");
|
||||||
|
assert(evaluateConditional("ifndef", "NONEXISTENT_VAR", "", &env),
|
||||||
|
"ifndef: NONEXISTENT_VAR missing should be true");
|
||||||
|
|
||||||
|
// --- ifdef with null env (no environment passed) ---
|
||||||
|
assert(!evaluateConditional("ifdef", "ANYTHING", ""),
|
||||||
|
"ifdef: null env should return false");
|
||||||
|
assert(evaluateConditional("ifndef", "ANYTHING", ""),
|
||||||
|
"ifndef: null env should return true");
|
||||||
|
|
||||||
|
// --- Mixed: ifneq with paren form ---
|
||||||
|
assert(evaluateConditional("ifneq", "(a, b)", ""),
|
||||||
|
"ifneq paren form: (a, b) should be true");
|
||||||
|
assert(!evaluateConditional("ifneq", "(a, a)", ""),
|
||||||
|
"ifneq paren form: (a, a) should be false");
|
||||||
|
|
||||||
|
// --- Empty strings ---
|
||||||
|
assert(evaluateConditional("ifeq", "", ""),
|
||||||
|
"ifeq: empty == empty should be true");
|
||||||
|
assert(!evaluateConditional("ifeq", "", "x"),
|
||||||
|
"ifeq: empty != x should be false");
|
||||||
|
|
||||||
|
writeln("All conditional evaluation tests passed.");
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,881 @@
|
|||||||
|
/// Variable expansion and substitution at evaluation time.
|
||||||
|
///
|
||||||
|
/// This module implements GNU Make-compatible recursive variable expansion.
|
||||||
|
/// It is the core string-processing engine responsible for resolving
|
||||||
|
/// `$(VAR)`, `$$`, automatic variables ($@, $<, etc.), D/F suffix extraction,
|
||||||
|
/// and `$(call ...)` substitution — all with circular-reference detection.
|
||||||
|
module antelope.evaluator.expansion;
|
||||||
|
|
||||||
|
import antelope.shell.environment;
|
||||||
|
import antelope.diagnostics.errors;
|
||||||
|
import antelope.parser.functions;
|
||||||
|
import std.string : lastIndexOf, strip, indexOf;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Expand all variable references in a string.
|
||||||
|
///
|
||||||
|
/// The expansion is recursive: when a variable's value itself contains
|
||||||
|
/// variable references, those are expanded in turn. Undefined variables
|
||||||
|
/// expand to the empty string (standard GNU Make behaviour). Circular
|
||||||
|
/// references (A → B → A) are detected and reported rather than looping.
|
||||||
|
///
|
||||||
|
/// Supported syntax:
|
||||||
|
/// * `$$` → literal `$`
|
||||||
|
/// * `$@` → current target name
|
||||||
|
/// * `$<` → first prerequisite
|
||||||
|
/// * `$^` → all prerequisites (unique, space-separated)
|
||||||
|
/// * `$+` → all prerequisites (duplicates preserved)
|
||||||
|
/// * `$?` → prerequisites newer than target (all prereqs for now)
|
||||||
|
/// * `$*` → pattern stem
|
||||||
|
/// * `$%` → archive member (not yet implemented → "")
|
||||||
|
/// * `$|` → order-only prerequisites (not yet implemented → "")
|
||||||
|
/// * `$(VAR)` → environment variable lookup + recursive expansion
|
||||||
|
/// * `${VAR}` → same as `$(VAR)`
|
||||||
|
/// * `$(@D)` / `$(@F)` → directory / file part of target
|
||||||
|
/// * `$(<D)` / `$(<F)` → directory / file part of first prerequisite
|
||||||
|
/// * `$(^D)` / `$(^F)` → directory / file parts of all prerequisites
|
||||||
|
/// * `$(*D)` / `$(*F)` → directory / file part of stem
|
||||||
|
/// * `$(call FUNC,arg1,arg2,…)` → call-style expansion
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// input = String to expand.
|
||||||
|
/// env = Pointer to the variable environment (may be null).
|
||||||
|
/// currentTarget = Value for `$@` (current target name).
|
||||||
|
/// currentPrereqs = Values for `$<`, `$^`, `$+`, `$?`.
|
||||||
|
/// stem = Value for `$*` (pattern stem).
|
||||||
|
///
|
||||||
|
/// Returns: The fully-expanded string.
|
||||||
|
string expand(string input, Environment* env, string currentTarget = "",
|
||||||
|
string[] currentPrereqs = [], string stem = "")
|
||||||
|
{
|
||||||
|
string[] chain;
|
||||||
|
return expandImpl(input, env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Directory part of a path (up to and including the last `/`, or `./`).
|
||||||
|
private string dirPart(string path)
|
||||||
|
{
|
||||||
|
auto idx = lastIndexOf(path, "/");
|
||||||
|
if (idx == size_t.max) // D's lastIndexOf returns size_t.max on no-match
|
||||||
|
return "./";
|
||||||
|
return path[0 .. idx + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File part of a path (everything after the last `/`).
|
||||||
|
private string filePart(string path)
|
||||||
|
{
|
||||||
|
auto idx = lastIndexOf(path, "/");
|
||||||
|
if (idx == size_t.max)
|
||||||
|
return path;
|
||||||
|
return path[idx + 1 .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply `dirPart` or `filePart` to each string in `values` and join with space.
|
||||||
|
private string applyDirFile(string[] values, bool wantDir)
|
||||||
|
{
|
||||||
|
if (values.length == 0)
|
||||||
|
return "";
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!string();
|
||||||
|
bool first = true;
|
||||||
|
foreach (v; values)
|
||||||
|
{
|
||||||
|
if (!first) buf.put(" ");
|
||||||
|
first = false;
|
||||||
|
buf.put(wantDir ? dirPart(v) : filePart(v));
|
||||||
|
}
|
||||||
|
return buf.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a single-character automatic variable.
|
||||||
|
/// Never returns null — always returns a string.
|
||||||
|
private string resolveAutoVar(char c, string currentTarget, string[] currentPrereqs,
|
||||||
|
string stem)
|
||||||
|
{
|
||||||
|
final switch (c)
|
||||||
|
{
|
||||||
|
case '@': return currentTarget;
|
||||||
|
case '<': return currentPrereqs.length > 0 ? currentPrereqs[0] : "";
|
||||||
|
case '^':
|
||||||
|
{
|
||||||
|
// Unique prerequisites (order preserved, first occurrence kept).
|
||||||
|
if (currentPrereqs.length == 0)
|
||||||
|
return "";
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!string();
|
||||||
|
bool[string] seen;
|
||||||
|
bool first = true;
|
||||||
|
foreach (p; currentPrereqs)
|
||||||
|
{
|
||||||
|
if (p in seen) continue;
|
||||||
|
seen[p] = true;
|
||||||
|
if (!first) buf.put(" ");
|
||||||
|
first = false;
|
||||||
|
buf.put(p);
|
||||||
|
}
|
||||||
|
return buf.data;
|
||||||
|
}
|
||||||
|
case '+': // All prereqs, duplicates preserved, space-separated.
|
||||||
|
{
|
||||||
|
if (currentPrereqs.length == 0)
|
||||||
|
return "";
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!string();
|
||||||
|
foreach (i, p; currentPrereqs)
|
||||||
|
{
|
||||||
|
if (i > 0) buf.put(" ");
|
||||||
|
buf.put(p);
|
||||||
|
}
|
||||||
|
return buf.data;
|
||||||
|
}
|
||||||
|
case '?':
|
||||||
|
{
|
||||||
|
// "Prereqs newer than target" — compare timestamps and
|
||||||
|
// return only those prerequisites that are newer than the
|
||||||
|
// target file. This matches GNU Make's $? semantics.
|
||||||
|
if (currentPrereqs.length == 0) return "";
|
||||||
|
import antelope.filesystem.timestamps;
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!string();
|
||||||
|
bool first = true;
|
||||||
|
long targetTime = getTimestamp(currentTarget);
|
||||||
|
foreach (p; currentPrereqs)
|
||||||
|
{
|
||||||
|
long pt = getTimestamp(p);
|
||||||
|
if (pt > targetTime || targetTime == -1)
|
||||||
|
{
|
||||||
|
if (!first) buf.put(" ");
|
||||||
|
first = false;
|
||||||
|
buf.put(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.data;
|
||||||
|
}
|
||||||
|
case '*': return stem;
|
||||||
|
case '%': return ""; // Archive member — not yet implemented.
|
||||||
|
case '|': return ""; // Order-only prereqs — not yet implemented.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve content found between `(` / `)` or `{` / `}`.
|
||||||
|
///
|
||||||
|
/// The content may be:
|
||||||
|
/// * `call FUNC,arg1,arg2,…` → call-style expansion
|
||||||
|
/// * `@D`, `<F`, `^D`, `*F`, etc. → automatic var D/F suffix
|
||||||
|
/// * `VAR` or `VAR_$(NESTED)` → regular variable lookup
|
||||||
|
///
|
||||||
|
/// `closer` is `)` or `}` (unused except for potential error messages).
|
||||||
|
private string resolveParenContent(string content, char closer, Environment* env,
|
||||||
|
string currentTarget, string[] currentPrereqs,
|
||||||
|
string stem, ref string[] chain)
|
||||||
|
{
|
||||||
|
// --- $(call FUNC,arg1,arg2,…) ------------------------------------------
|
||||||
|
if (content.length >= 5 && content[0 .. 5] == "call ")
|
||||||
|
{
|
||||||
|
return expandCall(content[5 .. $], env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Built-in function calls: $(shell ...), $(subst ...), etc. -----------
|
||||||
|
// Detect if the first word is a known GNU Make function name.
|
||||||
|
import std.string : indexOf;
|
||||||
|
auto firstSpace = indexOf(content, ' ');
|
||||||
|
string firstWord = firstSpace >= 0 ? content[0 .. firstSpace] : content;
|
||||||
|
if (isBuiltinFunction(firstWord))
|
||||||
|
{
|
||||||
|
return evaluateBuiltinCall(firstWord, content, env, currentTarget, currentPrereqs, stem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Automatic var with D/F suffix ($(@D), $(<F), $(^D), etc.) ---------
|
||||||
|
if (content.length == 2 &&
|
||||||
|
(content[0] == '@' || content[0] == '<' || content[0] == '^' ||
|
||||||
|
content[0] == '+' || content[0] == '?' || content[0] == '*') &&
|
||||||
|
(content[1] == 'D' || content[1] == 'F'))
|
||||||
|
{
|
||||||
|
bool wantDir = (content[1] == 'D');
|
||||||
|
char av = content[0];
|
||||||
|
|
||||||
|
// For multi-value automatic vars ($^D, $+D, $?D) we apply
|
||||||
|
// dirPart / filePart to each element.
|
||||||
|
if (av == '^' || av == '+' || av == '?')
|
||||||
|
{
|
||||||
|
string joined = resolveAutoVar(av, currentTarget, currentPrereqs, stem);
|
||||||
|
if (joined.length == 0) return "";
|
||||||
|
import std.array : split;
|
||||||
|
auto parts = split(joined, " ");
|
||||||
|
return applyDirFile(parts, wantDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-value automatic vars ($@, $<, $*)
|
||||||
|
string single = resolveAutoVar(av, currentTarget, currentPrereqs, stem);
|
||||||
|
if (single.length == 0) return "";
|
||||||
|
return wantDir ? dirPart(single) : filePart(single);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Substitution reference: $(VAR:old=new) → $(patsubst old,new,$(VAR)) ---
|
||||||
|
auto colonIdx = indexOf(content, ':');
|
||||||
|
if (colonIdx >= 0 && colonIdx < content.length - 1 &&
|
||||||
|
content[colonIdx + 1] != '=' && indexOf(content[colonIdx + 1 .. $], '=') >= 0)
|
||||||
|
{
|
||||||
|
// Syntax: VAR:old=new
|
||||||
|
string varName = content[0 .. colonIdx];
|
||||||
|
auto eqIdx = indexOf(content[colonIdx + 1 .. $], '=');
|
||||||
|
string oldPat = content[colonIdx + 1 .. colonIdx + 1 + eqIdx];
|
||||||
|
string newPat = content[colonIdx + 1 + eqIdx + 1 .. $];
|
||||||
|
// Equivalent to $(patsubst oldPat,newPat,$(varName))
|
||||||
|
string varValue = env ? env.get(varName) : "";
|
||||||
|
if (varValue.length > 0)
|
||||||
|
{
|
||||||
|
import antelope.evaluator.functions;
|
||||||
|
import antelope.parser.functions;
|
||||||
|
return evaluateFunction(BuiltinFunction.patsubst, [oldPat, newPat, varValue], env);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Regular variable lookup -------------------------------------------
|
||||||
|
// First, recursively expand any nested `$` references in the content.
|
||||||
|
// This handles $(VAR_$(NESTED)) — expand $(NESTED) first, then look up.
|
||||||
|
string expandedName = expandImpl(content, env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
|
||||||
|
if (expandedName.length == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// Environment lookup — consult target-scoped variables first.
|
||||||
|
string value = env ? env.getScoped(expandedName, currentTarget) : "";
|
||||||
|
if (value.length == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// Circular-reference guard.
|
||||||
|
foreach (c; chain)
|
||||||
|
{
|
||||||
|
if (c == expandedName)
|
||||||
|
{
|
||||||
|
// Found a cycle — report and bail out.
|
||||||
|
// (In the future this could use a diagnostic emitter.)
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain ~= expandedName;
|
||||||
|
scope (exit) chain = chain[0 .. $ - 1];
|
||||||
|
return expandImpl(value, env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand a `$(call FUNC,arg1,arg2,…)` reference.
|
||||||
|
///
|
||||||
|
/// `rest` is everything after `"call "` — i.e. `"FUNC,arg1,arg2,…"`.
|
||||||
|
/// Steps:
|
||||||
|
/// 1. Split on top-level commas to get variable name + args.
|
||||||
|
/// 2. Expand the variable name to find which variable to invoke.
|
||||||
|
/// 3. Look up its value from the environment.
|
||||||
|
/// 4. Substitute `$1`, `$2`, … in the value with the expanded args.
|
||||||
|
/// 5. Recursively expand the result.
|
||||||
|
private string expandCall(string rest, Environment* env,
|
||||||
|
string currentTarget, string[] currentPrereqs,
|
||||||
|
string stem, ref string[] chain)
|
||||||
|
{
|
||||||
|
// Split `FUNC,arg1,arg2,…` on top-level commas.
|
||||||
|
string[] parts;
|
||||||
|
{
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!(string[])();
|
||||||
|
size_t pos = 0;
|
||||||
|
size_t depth = 0;
|
||||||
|
size_t start = 0;
|
||||||
|
while (pos < rest.length)
|
||||||
|
{
|
||||||
|
char ch = rest[pos];
|
||||||
|
if (ch == '(' || ch == '{') depth++;
|
||||||
|
else if (ch == ')' || ch == '}') depth--;
|
||||||
|
else if (ch == ',' && depth == 0)
|
||||||
|
{
|
||||||
|
buf.put(rest[start .. pos]);
|
||||||
|
start = pos + 1;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
// Last segment.
|
||||||
|
buf.put(rest[start .. $]);
|
||||||
|
parts = buf.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// Parts[0] is the function name — expand it to get the variable name.
|
||||||
|
string funcName = expandImpl(parts[0], env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
|
||||||
|
if (funcName.length == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// Look up the function body.
|
||||||
|
string body = env ? env.get(funcName) : "";
|
||||||
|
if (body.length == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// Expand each argument.
|
||||||
|
string[] expandedArgs;
|
||||||
|
foreach (i; 1 .. parts.length)
|
||||||
|
{
|
||||||
|
expandedArgs ~= expandImpl(parts[i], env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute $1, $2, … in the body.
|
||||||
|
// We scan the body character by character for $N patterns.
|
||||||
|
string bodyWithSubs = substituteCallArgs(body, expandedArgs);
|
||||||
|
|
||||||
|
// Recursively expand the result (it may contain further variable refs).
|
||||||
|
return expandImpl(bodyWithSubs, env, currentTarget, currentPrereqs, stem, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Substitute `$1`, `$2`, …, `$0` (the function name) in `body` with the
|
||||||
|
/// corresponding values from `args`.
|
||||||
|
///
|
||||||
|
/// `args[0]` corresponds to `$1`, `args[1]` to `$2`, etc.
|
||||||
|
/// `$0` is set to the function name (already resolved).
|
||||||
|
///
|
||||||
|
/// `$$` in the body is left as a literal `$` (it was already quoted).
|
||||||
|
/// Unknown `$N` is replaced by the empty string.
|
||||||
|
private string substituteCallArgs(string body, string[] args)
|
||||||
|
{
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!string();
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < body.length)
|
||||||
|
{
|
||||||
|
if (body[i] == '$')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
if (i >= body.length)
|
||||||
|
{
|
||||||
|
buf.put('$');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char c = body[i];
|
||||||
|
if (c == '$')
|
||||||
|
{
|
||||||
|
buf.put('$');
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Is it a digit? $0, $1, …, $9
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
{
|
||||||
|
uint n = c - '0';
|
||||||
|
if (n == 0)
|
||||||
|
{
|
||||||
|
// $0 is replaced by empty string (or function name — but
|
||||||
|
// GNU Make doesn't use $0 in call; it's the function name
|
||||||
|
// and is not substituted). Leave as empty for now.
|
||||||
|
buf.put("");
|
||||||
|
}
|
||||||
|
else if (n <= args.length)
|
||||||
|
{
|
||||||
|
buf.put(args[n - 1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unknown $N → ""
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Not a digit — output $ and the char (e.g. $( inside body).
|
||||||
|
buf.put('$');
|
||||||
|
buf.put(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buf.put(body[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core expansion implementation — recursive, with chain tracking for
|
||||||
|
/// circular-reference detection.
|
||||||
|
private string expandImpl(string input, Environment* env, string currentTarget,
|
||||||
|
string[] currentPrereqs, string stem, ref string[] chain)
|
||||||
|
{
|
||||||
|
import std.array : appender;
|
||||||
|
auto buf = appender!string();
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
while (i < input.length)
|
||||||
|
{
|
||||||
|
if (input[i] == '$')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
if (i >= input.length)
|
||||||
|
{
|
||||||
|
buf.put('$');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = input[i];
|
||||||
|
|
||||||
|
// $$ → literal $
|
||||||
|
if (c == '$')
|
||||||
|
{
|
||||||
|
buf.put('$');
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-character $X variable references.
|
||||||
|
// $@, $<, $^, etc. → automatic variables
|
||||||
|
if (c == '@' || c == '<' || c == '^' || c == '+' ||
|
||||||
|
c == '?' || c == '*' || c == '%' || c == '|')
|
||||||
|
{
|
||||||
|
buf.put(resolveAutoVar(c, currentTarget, currentPrereqs, stem));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// $1..$9, $0 → positional parameters (call, foreach, etc.)
|
||||||
|
// and any other single-char variable reference
|
||||||
|
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
|
||||||
|
{
|
||||||
|
string varName = [c];
|
||||||
|
string val = env ? env.get(varName) : "";
|
||||||
|
buf.put(val);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $(…) or ${…}
|
||||||
|
if (c == '(' || c == '{')
|
||||||
|
{
|
||||||
|
char openChar = c;
|
||||||
|
char closeChar = (c == '(') ? ')' : '}';
|
||||||
|
i++; // skip opening delimiter
|
||||||
|
|
||||||
|
size_t contentStart = i;
|
||||||
|
int depth = 1;
|
||||||
|
while (i < input.length && depth > 0)
|
||||||
|
{
|
||||||
|
if (input[i] == openChar)
|
||||||
|
depth++;
|
||||||
|
else if (input[i] == closeChar)
|
||||||
|
depth--;
|
||||||
|
if (depth > 0)
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= input.length)
|
||||||
|
{
|
||||||
|
// Unterminated $('… or ${… — output literally.
|
||||||
|
buf.put('$');
|
||||||
|
buf.put(openChar);
|
||||||
|
buf.put(input[contentStart .. $]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string content = input[contentStart .. i];
|
||||||
|
i++; // skip closing delimiter
|
||||||
|
|
||||||
|
buf.put(resolveParenContent(content, closeChar, env, currentTarget,
|
||||||
|
currentPrereqs, stem, chain));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown $X — output literally.
|
||||||
|
buf.put('$');
|
||||||
|
buf.put(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buf.put(input[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unittests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// --- Simple $(VAR) expansion ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("FOO", "bar");
|
||||||
|
string result = expand("$(FOO)", &env);
|
||||||
|
assert(result == "bar", "Simple $(FOO) should expand to 'bar', got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ${VAR} syntax ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("FOO", "baz");
|
||||||
|
string result = expand("${FOO}", &env);
|
||||||
|
assert(result == "baz", "${FOO} should expand to 'baz', got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $$ → literal $ ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("prefix $$ suffix", &env);
|
||||||
|
assert(result == "prefix $ suffix", "$$ should become '$', got: " ~ result);
|
||||||
|
|
||||||
|
result = expand("$$$$", &env);
|
||||||
|
assert(result == "$$", "$$$$ should become '$$', got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $@ → current target ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$@", &env, "myfile.o");
|
||||||
|
assert(result == "myfile.o", "$@ should be target name, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $< → first prerequisite ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$<", &env, "", ["a.c", "b.c"]);
|
||||||
|
assert(result == "a.c", "$< should be first prereq, got: " ~ result);
|
||||||
|
|
||||||
|
result = expand("$<", &env, "", []);
|
||||||
|
assert(result == "", "$< with no prereqs should be empty, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $^ → all prereqs, unique ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$^", &env, "", ["a.c", "b.c", "a.c"]);
|
||||||
|
assert(result == "a.c b.c", "$^ should dedupe prereqs, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $+ → all prereqs, duplicates preserved ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$+", &env, "", ["a.c", "b.c", "a.c"]);
|
||||||
|
assert(result == "a.c b.c a.c", "$+ should preserve duplicates, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $* → stem ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$*", &env, "", [], "foo");
|
||||||
|
assert(result == "foo", "$* should be stem, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $% → "" (not implemented) ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$%", &env);
|
||||||
|
assert(result == "", "$% should be empty (not implemented), got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $| → "" (not implemented) ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$|", &env);
|
||||||
|
assert(result == "", "$| should be empty (not implemented), got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(@D) → directory part of target ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$(@D)", &env, "src/sub/file.o");
|
||||||
|
assert(result == "src/sub/", "$(@D) should be dir part, got: " ~ result);
|
||||||
|
|
||||||
|
result = expand("$(@D)", &env, "file.o");
|
||||||
|
assert(result == "./", "$(@D) with no slash should be './', got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(@F) → file part of target ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$(@F)", &env, "src/sub/file.o");
|
||||||
|
assert(result == "file.o", "$(@F) should be file part, got: " ~ result);
|
||||||
|
|
||||||
|
result = expand("$(@F)", &env, "file.o");
|
||||||
|
assert(result == "file.o", "$(@F) with no slash should be full name, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(<D) and $(<F) ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$(<D)", &env, "", ["src/a.c"]);
|
||||||
|
assert(result == "src/", "$(<D) should be dir part of first prereq, got: " ~ result);
|
||||||
|
|
||||||
|
result = expand("$(<F)", &env, "", ["src/a.c"]);
|
||||||
|
assert(result == "a.c", "$(<F) should be file part of first prereq, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(^D) and $(^F) ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$(^D)", &env, "", ["src/a.c", "inc/b.h"]);
|
||||||
|
assert(result == "src/ inc/", "$(^D) with multi prereqs, got: " ~ result);
|
||||||
|
|
||||||
|
result = expand("$(^F)", &env, "", ["src/a.c", "inc/b.h"]);
|
||||||
|
assert(result == "a.c b.h", "$(^F) with multi prereqs, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Nested expansion: $(VAR_$(NESTED)) ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("SUFFIX", "FLAGS");
|
||||||
|
env.set("CXXFLAGS", "-O2 -Wall");
|
||||||
|
string result = expand("$(CXX$(SUFFIX))", &env);
|
||||||
|
assert(result == "-O2 -Wall", "Nested expansion failed, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Undefined variable → "" ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("$(UNDEFINED_VAR)", &env);
|
||||||
|
assert(result == "", "Undefined var should be empty, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(call ...) basic ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("reverse", "$2 $1");
|
||||||
|
string result = expand("$(call reverse,a,b)", &env);
|
||||||
|
assert(result == "b a", "call reverse failed, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(call ...) with extra args (unused $N → "") ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("prefix", "[$1]");
|
||||||
|
string result = expand("$(call prefix,hello)", &env);
|
||||||
|
assert(result == "[hello]", "call prefix failed, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $(call ...) with nested $ in args ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("VAL", "xyz");
|
||||||
|
env.set("wrap", "<$1>");
|
||||||
|
string result = expand("$(call wrap,$(VAL))", &env);
|
||||||
|
assert(result == "<xyz>", "call with nested var in arg failed, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Circular reference detection ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("A", "$(B)");
|
||||||
|
env.set("B", "$(A)");
|
||||||
|
string result = expand("$(A)", &env);
|
||||||
|
assert(result == "", "Circular A→B→A should return empty, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Circular self-reference ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("X", "$(X)");
|
||||||
|
string result = expand("$(X)", &env);
|
||||||
|
assert(result == "", "Self-circular X→X should return empty, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Deeply nested expansion ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("A", "1$(B)");
|
||||||
|
env.set("B", "2$(C)");
|
||||||
|
env.set("C", "3");
|
||||||
|
string result = expand("$(A)", &env);
|
||||||
|
assert(result == "123", "Deeply nested A→B→C failed, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Multiple $ in same string ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("NAME", "Antelope");
|
||||||
|
env.set("VER", "1.0");
|
||||||
|
string result = expand("$(NAME) v$(VER) $$HOME", &env);
|
||||||
|
assert(result == "Antelope v1.0 $HOME",
|
||||||
|
"Multiple $ expansion failed, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Literal string passthrough (no $) ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
string result = expand("hello world", &env);
|
||||||
|
assert(result == "hello world", "Plain string should pass through, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- $? (newer prereqs) — all prereqs returned when target doesn't exist ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
// Both target ("") and prereqs ("a.c", "b.c") are non-existent files,
|
||||||
|
// so getTimestamp returns -1 for all. Since targetTime == -1,
|
||||||
|
// all prereqs are considered newer.
|
||||||
|
string result = expand("$?", &env, "", ["a.c", "b.c", "a.c"]);
|
||||||
|
assert(result == "a.c b.c a.c", "$? should return all when target absent, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Null env pointer — all vars → "" ---
|
||||||
|
{
|
||||||
|
string result = expand("$(ANYTHING)", null);
|
||||||
|
assert(result == "", "Null env should treat all vars as undefined, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- call with $0 → empty ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("showall", "$0 $1 $2");
|
||||||
|
string result = expand("$(call showall,foo,bar)", &env);
|
||||||
|
// $0 is empty, $1=foo, $2=bar
|
||||||
|
assert(result == " foo bar", "call with $0, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- call with nested function name expansion ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("WHICH", "upper");
|
||||||
|
env.set("upper", "[$1]");
|
||||||
|
string result = expand("$(call $(WHICH),text)", &env);
|
||||||
|
assert(result == "[text]", "call with nested func name, got: " ~ result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Unclosed $( → output literally ---
|
||||||
|
{
|
||||||
|
Environment env;
|
||||||
|
env.set("FOO", "bar");
|
||||||
|
string result = expand("start $(FOO", &env);
|
||||||
|
assert(result == "start $(FOO", "Unclosed $( should be literal, got: " ~ result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a word is a known GNU Make built-in function name.
|
||||||
|
bool isBuiltinFunction(string word)
|
||||||
|
{
|
||||||
|
switch (word)
|
||||||
|
{
|
||||||
|
case "subst": case "patsubst": case "strip": case "findstring":
|
||||||
|
case "filter": case "filter-out": case "sort": case "word":
|
||||||
|
case "words": case "wordlist": case "firstword": case "lastword":
|
||||||
|
case "dir": case "notdir": case "suffix": case "basename":
|
||||||
|
case "addsuffix": case "addprefix": case "join":
|
||||||
|
case "wildcard": case "realpath": case "abspath":
|
||||||
|
case "shell": case "error": case "warning": case "info":
|
||||||
|
case "foreach": case "call": case "value": case "origin": case "flavor":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a built-in function call from expansion context.
|
||||||
|
private string evaluateBuiltinCall(string funcName, string content, Environment* env,
|
||||||
|
string currentTarget = "", string[] currentPrereqs = [], string stem = "")
|
||||||
|
{
|
||||||
|
import antelope.evaluator.functions;
|
||||||
|
import antelope.parser.functions;
|
||||||
|
|
||||||
|
// Parse arguments with nesting-aware comma splitting.
|
||||||
|
// Commas inside nested $(...) or ${...} are NOT argument separators.
|
||||||
|
string[] args;
|
||||||
|
size_t start = funcName.length;
|
||||||
|
if (start < content.length && content[start] == ' ')
|
||||||
|
start++;
|
||||||
|
|
||||||
|
string rest = content[start .. $];
|
||||||
|
size_t segStart = 0;
|
||||||
|
int depth = 0; // paren/brace nesting depth
|
||||||
|
for (size_t i = 0; i < rest.length; i++)
|
||||||
|
{
|
||||||
|
char c = rest[i];
|
||||||
|
if (c == '(' || c == '{') depth++;
|
||||||
|
else if (c == ')' || c == '}') { if (depth > 0) depth--; }
|
||||||
|
else if (c == ',' && depth == 0)
|
||||||
|
{
|
||||||
|
args ~= rest[segStart .. i].strip;
|
||||||
|
segStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last segment
|
||||||
|
if (segStart < rest.length)
|
||||||
|
args ~= rest[segStart .. $].strip;
|
||||||
|
|
||||||
|
// Recursively expand each argument before dispatching to the function.
|
||||||
|
// Pass currentTarget/prereqs/stem so $@, $<, $^ work inside function args.
|
||||||
|
// Functions that manage their own expansion (foreach, if, or, and)
|
||||||
|
// receive raw (unexpanded) arguments.
|
||||||
|
import antelope.parser.functions;
|
||||||
|
BuiltinFunction bf = builtinFromName(funcName);
|
||||||
|
bool preExpand = !isSelfExpanding(bf);
|
||||||
|
string[] expandedArgs;
|
||||||
|
foreach (arg; args)
|
||||||
|
{
|
||||||
|
if (preExpand)
|
||||||
|
expandedArgs ~= expand(arg, env, currentTarget, currentPrereqs, stem);
|
||||||
|
else
|
||||||
|
expandedArgs ~= arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluateFunction(bf, expandedArgs, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true for functions that manage their own argument expansion
|
||||||
|
/// (matching GNU Make's `expand_args=0` flag).
|
||||||
|
private bool isSelfExpanding(BuiltinFunction bf)
|
||||||
|
{
|
||||||
|
import antelope.parser.functions;
|
||||||
|
switch (bf)
|
||||||
|
{
|
||||||
|
case BuiltinFunction.foreach_:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map function name string to BuiltinFunction enum.
|
||||||
|
BuiltinFunction builtinFromName(string name)
|
||||||
|
{
|
||||||
|
import antelope.parser.functions;
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case "subst": return BuiltinFunction.subst;
|
||||||
|
case "patsubst": return BuiltinFunction.patsubst;
|
||||||
|
case "strip": return BuiltinFunction.strip;
|
||||||
|
case "findstring": return BuiltinFunction.findstring;
|
||||||
|
case "filter": return BuiltinFunction.filter;
|
||||||
|
case "filter-out": return BuiltinFunction.filter_out;
|
||||||
|
case "sort": return BuiltinFunction.sort;
|
||||||
|
case "word": return BuiltinFunction.word;
|
||||||
|
case "words": return BuiltinFunction.words;
|
||||||
|
case "wordlist": return BuiltinFunction.wordlist;
|
||||||
|
case "firstword": return BuiltinFunction.firstword;
|
||||||
|
case "lastword": return BuiltinFunction.lastword;
|
||||||
|
case "dir": return BuiltinFunction.dir;
|
||||||
|
case "notdir": return BuiltinFunction.notdir;
|
||||||
|
case "suffix": return BuiltinFunction.suffix;
|
||||||
|
case "basename": return BuiltinFunction.basename;
|
||||||
|
case "addsuffix": return BuiltinFunction.addsuffix;
|
||||||
|
case "addprefix": return BuiltinFunction.addprefix;
|
||||||
|
case "join": return BuiltinFunction.join;
|
||||||
|
case "wildcard": return BuiltinFunction.wildcard;
|
||||||
|
case "realpath": return BuiltinFunction.realpath;
|
||||||
|
case "abspath": return BuiltinFunction.abspath;
|
||||||
|
case "shell": return BuiltinFunction.shell;
|
||||||
|
case "error": return BuiltinFunction.error;
|
||||||
|
case "warning": return BuiltinFunction.warning;
|
||||||
|
case "info": return BuiltinFunction.info;
|
||||||
|
case "foreach": return BuiltinFunction.foreach_;
|
||||||
|
case "call": return BuiltinFunction.call;
|
||||||
|
case "value": return BuiltinFunction.value;
|
||||||
|
case "origin": return BuiltinFunction.origin;
|
||||||
|
case "flavor": return BuiltinFunction.flavor;
|
||||||
|
default: return BuiltinFunction.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
|||||||
|
/// File discovery and globbing utilities.
|
||||||
|
module antelope.filesystem.files;
|
||||||
|
|
||||||
|
import std.algorithm;
|
||||||
|
import std.array;
|
||||||
|
import std.file;
|
||||||
|
import std.path;
|
||||||
|
import std.string;
|
||||||
|
|
||||||
|
/// Find files matching a glob pattern.
|
||||||
|
///
|
||||||
|
/// Supports `*` (single-directory wildcard) and `**` (recursive wildcard).
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// ---
|
||||||
|
/// glob("*.d") // all D files in current directory
|
||||||
|
/// glob("src/**/*.d") // all D files under src/ recursively
|
||||||
|
/// glob("**") // all files recursively from current directory
|
||||||
|
/// ---
|
||||||
|
///
|
||||||
|
/// Returns a sorted array of relative paths, or an empty array if no
|
||||||
|
/// matches are found or the directory does not exist.
|
||||||
|
string[] glob(string pattern)
|
||||||
|
{
|
||||||
|
string dir;
|
||||||
|
string filePattern;
|
||||||
|
SpanMode mode;
|
||||||
|
|
||||||
|
auto doubleStar = pattern.indexOf("**");
|
||||||
|
if (doubleStar >= 0)
|
||||||
|
{
|
||||||
|
// Extract directory prefix before **
|
||||||
|
dir = pattern[0 .. doubleStar];
|
||||||
|
if (dir.length == 0)
|
||||||
|
dir = ".";
|
||||||
|
else if (dir[$ - 1] == '/' || dir[$ - 1] == '\\')
|
||||||
|
dir = dir[0 .. $ - 1];
|
||||||
|
|
||||||
|
// Extract file pattern after **/
|
||||||
|
filePattern = pattern[doubleStar + 2 .. $];
|
||||||
|
while (filePattern.length > 0
|
||||||
|
&& (filePattern[0] == '/' || filePattern[0] == '\\'))
|
||||||
|
filePattern = filePattern[1 .. $];
|
||||||
|
|
||||||
|
// Bare ** matches everything recursively
|
||||||
|
if (filePattern.length == 0)
|
||||||
|
filePattern = "*";
|
||||||
|
|
||||||
|
mode = SpanMode.depth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dir = pattern.dirName();
|
||||||
|
if (dir.length == 0)
|
||||||
|
dir = ".";
|
||||||
|
filePattern = pattern.baseName();
|
||||||
|
mode = SpanMode.shallow;
|
||||||
|
|
||||||
|
// When directory contains wildcards (e.g., "src/*"),
|
||||||
|
// dirEntries can't use them as literal paths.
|
||||||
|
// Fall back to depth-scan from the last non-wildcard base.
|
||||||
|
if (dir.indexOf('*') >= 0 || dir.indexOf('?') >= 0)
|
||||||
|
{
|
||||||
|
// Find the last component before any wildcard
|
||||||
|
import std.path : dirSeparator;
|
||||||
|
auto slash = dir.lastIndexOf('/');
|
||||||
|
if (slash < 0) slash = dir.lastIndexOf('\\');
|
||||||
|
string baseDir = (slash < 0) ? "." : dir[0 .. slash];
|
||||||
|
if (baseDir.length == 0) baseDir = ".";
|
||||||
|
|
||||||
|
// Use depth scan from base, filter by filename pattern
|
||||||
|
mode = SpanMode.depth;
|
||||||
|
dir = baseDir;
|
||||||
|
// filePattern already set to basename (e.g., "*.c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto entries = dirEntries(dir, filePattern, mode)
|
||||||
|
.map!(e => e.name)
|
||||||
|
.array;
|
||||||
|
sort(entries);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coverage for glob function.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
import std.file : mkdir, rmdir, remove;
|
||||||
|
import std.path : buildPath;
|
||||||
|
|
||||||
|
auto testDir = "glob_test_temp_xx";
|
||||||
|
|
||||||
|
// Ensure clean state.
|
||||||
|
scope (exit)
|
||||||
|
{
|
||||||
|
if (exists(buildPath(testDir, "sub")))
|
||||||
|
rmdir(buildPath(testDir, "sub"));
|
||||||
|
if (exists(testDir))
|
||||||
|
rmdir(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir(testDir);
|
||||||
|
std.file.write(buildPath(testDir, "foo.d"), "");
|
||||||
|
std.file.write(buildPath(testDir, "bar.d"), "");
|
||||||
|
std.file.write(buildPath(testDir, "baz.txt"), "");
|
||||||
|
|
||||||
|
mkdir(buildPath(testDir, "sub"));
|
||||||
|
std.file.write(buildPath(testDir, "sub", "qux.d"), "");
|
||||||
|
|
||||||
|
// Shallow glob — matches *.d in test dir only.
|
||||||
|
auto dFiles = glob(buildPath(testDir, "*.d"));
|
||||||
|
assert(dFiles.length == 2);
|
||||||
|
assert(dFiles.canFind(buildPath(testDir, "foo.d")));
|
||||||
|
assert(dFiles.canFind(buildPath(testDir, "bar.d")));
|
||||||
|
|
||||||
|
// Recursive glob — matches all .d files under test dir.
|
||||||
|
auto allDFiles = glob(buildPath(testDir, "**/*.d"));
|
||||||
|
assert(allDFiles.length == 3);
|
||||||
|
assert(allDFiles.canFind(buildPath(testDir, "sub", "qux.d")));
|
||||||
|
|
||||||
|
// No matches returns empty array.
|
||||||
|
auto noMatch = glob(buildPath(testDir, "*.xyz"));
|
||||||
|
assert(noMatch.length == 0);
|
||||||
|
|
||||||
|
// Nonexistent directory returns empty array.
|
||||||
|
auto badDir = glob(buildPath("nonexistent_dir_abcdef", "*.d"));
|
||||||
|
assert(badDir.length == 0);
|
||||||
|
|
||||||
|
// Clean up test files.
|
||||||
|
remove(buildPath(testDir, "foo.d"));
|
||||||
|
remove(buildPath(testDir, "bar.d"));
|
||||||
|
remove(buildPath(testDir, "baz.txt"));
|
||||||
|
remove(buildPath(testDir, "sub", "qux.d"));
|
||||||
|
rmdir(buildPath(testDir, "sub"));
|
||||||
|
rmdir(testDir);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/// Path resolution, normalization, and working-directory tracking.
|
||||||
|
module antelope.filesystem.paths;
|
||||||
|
|
||||||
|
import std.path : absolutePath, buildNormalizedPath;
|
||||||
|
import std.file : exists, readLink;
|
||||||
|
|
||||||
|
/// Resolve a path to its canonical absolute form.
|
||||||
|
///
|
||||||
|
/// Converts relative paths to absolute, normalizes `.` and `..` segments,
|
||||||
|
/// and resolves symlinks if the path exists. If the path does not exist,
|
||||||
|
/// returns the best-effort normalized absolute form.
|
||||||
|
///
|
||||||
|
/// Returns: The canonical absolute form of `path`.
|
||||||
|
string resolvePath(string path)
|
||||||
|
{
|
||||||
|
// Resolve relative to working directory
|
||||||
|
string result = absolutePath(path);
|
||||||
|
|
||||||
|
// Normalize `.` and `..` segments
|
||||||
|
result = buildNormalizedPath(result);
|
||||||
|
|
||||||
|
// Resolve symlinks if the path exists
|
||||||
|
if (exists(result))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = readLink(result);
|
||||||
|
// readLink may return a relative path; make it absolute
|
||||||
|
result = buildNormalizedPath(absolutePath(result));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Not a symlink or readLink failed — keep the normalized path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assert(resolvePath(".").length > 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/// File timestamp comparison for out-of-date detection.
|
||||||
|
module antelope.filesystem.timestamps;
|
||||||
|
|
||||||
|
import std.file;
|
||||||
|
import std.datetime;
|
||||||
|
import antelope.compatibility.vpath;
|
||||||
|
|
||||||
|
/// Get the last-modified time of a file as a Unix timestamp.
|
||||||
|
///
|
||||||
|
/// Returns: The modification time in seconds since the Unix epoch
|
||||||
|
/// (1970-01-01T00:00:00Z), or -1 if the file does not
|
||||||
|
/// exist or cannot be accessed (e.g., permission denied).
|
||||||
|
long getTimestamp(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto mtime = timeLastModified(path);
|
||||||
|
return mtime.toUnixTime();
|
||||||
|
}
|
||||||
|
catch (FileException)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine whether a target needs to be rebuilt based on its
|
||||||
|
/// prerequisites.
|
||||||
|
///
|
||||||
|
/// A target is considered out of date (returning true) when:
|
||||||
|
/// - It is listed in the phonyTargets set (always rebuild).
|
||||||
|
/// - The target file does not exist.
|
||||||
|
/// - Any prerequisite file does not exist (after VPATH search).
|
||||||
|
/// - Any prerequisite has a newer timestamp than the target.
|
||||||
|
///
|
||||||
|
/// Order-only prerequisites (those after `|` in the rule) are NOT
|
||||||
|
/// checked for timestamps — they must exist but their modification
|
||||||
|
/// time does not trigger a rebuild.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// target = The file or phony target to check.
|
||||||
|
/// prerequisites = List of prerequisite file paths.
|
||||||
|
/// phonySet = Optional set of phony target names (may be null).
|
||||||
|
/// vpath = Optional VPATH config for prerequisite search (may be null).
|
||||||
|
/// orderOnlyPrereqs = Optional list of order-only prereqs to skip (may be null).
|
||||||
|
///
|
||||||
|
/// Returns: true if the target needs to be rebuilt, false otherwise.
|
||||||
|
bool needsRebuild(string target, string[] prerequisites,
|
||||||
|
const bool[string]* phonySet = null,
|
||||||
|
const(VPathConfig)* vpathConfig = null,
|
||||||
|
const string[]* orderOnlyPrereqs = null)
|
||||||
|
{
|
||||||
|
// Phony targets always need rebuilding.
|
||||||
|
if (phonySet !is null && (target in *phonySet) !is null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
auto targetTime = getTimestamp(target);
|
||||||
|
|
||||||
|
// Non-existent target must be built.
|
||||||
|
if (targetTime == -1)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
foreach (prereq; prerequisites)
|
||||||
|
{
|
||||||
|
// Skip order-only prerequisites — they must exist but their
|
||||||
|
// timestamps should NOT trigger a rebuild.
|
||||||
|
if (orderOnlyPrereqs !is null)
|
||||||
|
{
|
||||||
|
bool isOrderOnly;
|
||||||
|
foreach (oo; *orderOnlyPrereqs)
|
||||||
|
{
|
||||||
|
if (prereq == oo)
|
||||||
|
{
|
||||||
|
isOrderOnly = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isOrderOnly)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try VPATH resolution if config provided
|
||||||
|
string resolved = prereq;
|
||||||
|
if (vpathConfig !is null)
|
||||||
|
resolved = vpathResolve(prereq, *vpathConfig);
|
||||||
|
|
||||||
|
auto prereqTime = getTimestamp(resolved);
|
||||||
|
|
||||||
|
// Missing prerequisite forces a rebuild.
|
||||||
|
if (prereqTime == -1)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Prerequisite newer than target → out of date.
|
||||||
|
if (prereqTime > targetTime)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unittest
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// getTimestamp returns -1 for non-existent files
|
||||||
|
assert(getTimestamp("/nonexistent_path_xyz_antelope_test") == -1);
|
||||||
|
|
||||||
|
// needsRebuild for non-existent target
|
||||||
|
assert(needsRebuild("/nonexistent_target_antelope_test", []));
|
||||||
|
|
||||||
|
// needsRebuild for phony target (using local phony set)
|
||||||
|
bool[string] phonySet = ["testPhony_antelope": true];
|
||||||
|
assert(needsRebuild("testPhony_antelope", [], &phonySet));
|
||||||
|
|
||||||
|
// needsRebuild with missing prerequisite
|
||||||
|
assert(needsRebuild("/nonexistent_target_foo", ["/nonexistent_prereq_bar"]));
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/// Abstract Syntax Tree node definitions.
|
||||||
|
module antelope.parser.ast;
|
||||||
|
|
||||||
|
/// All node kinds in the AST.
|
||||||
|
enum AstType
|
||||||
|
{
|
||||||
|
rule_list,
|
||||||
|
rule,
|
||||||
|
prerequisite,
|
||||||
|
recipe_line,
|
||||||
|
variable_assignment,
|
||||||
|
directive,
|
||||||
|
function_call,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A node in the AST.
|
||||||
|
struct AstNode
|
||||||
|
{
|
||||||
|
AstType type;
|
||||||
|
AstNode[] children;
|
||||||
|
string data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/// Parser support for Makefile directives (include, ifdef, ifeq, etc.).
|
||||||
|
module antelope.parser.directives;
|
||||||
|
|
||||||
|
/// Supported directives.
|
||||||
|
enum DirectiveType
|
||||||
|
{
|
||||||
|
include,
|
||||||
|
define,
|
||||||
|
undefine,
|
||||||
|
ifdef,
|
||||||
|
ifndef,
|
||||||
|
ifeq,
|
||||||
|
ifneq,
|
||||||
|
else_,
|
||||||
|
endif,
|
||||||
|
export_,
|
||||||
|
unexport,
|
||||||
|
vpath,
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/// Parsing for GNU Make function calls: $(func ...).
|
||||||
|
module antelope.parser.functions;
|
||||||
|
|
||||||
|
/// Known GNU Make functions.
|
||||||
|
enum BuiltinFunction
|
||||||
|
{
|
||||||
|
subst,
|
||||||
|
patsubst,
|
||||||
|
strip,
|
||||||
|
findstring,
|
||||||
|
filter,
|
||||||
|
filter_out,
|
||||||
|
sort,
|
||||||
|
word,
|
||||||
|
words,
|
||||||
|
wordlist,
|
||||||
|
firstword,
|
||||||
|
lastword,
|
||||||
|
dir,
|
||||||
|
notdir,
|
||||||
|
suffix,
|
||||||
|
basename,
|
||||||
|
addsuffix,
|
||||||
|
addprefix,
|
||||||
|
join,
|
||||||
|
wildcard,
|
||||||
|
realpath,
|
||||||
|
abspath,
|
||||||
|
shell,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
foreach_,
|
||||||
|
call,
|
||||||
|
value,
|
||||||
|
origin,
|
||||||
|
flavor,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed function call.
|
||||||
|
struct FunctionCall
|
||||||
|
{
|
||||||
|
BuiltinFunction func;
|
||||||
|
string[] arguments;
|
||||||
|
}
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
/// Lexical analysis for Antelope build files.
|
||||||
|
///
|
||||||
|
/// Tokenizes GNU Make-compatible syntax including tab-prefixed recipe lines,
|
||||||
|
/// backslash-newline line continuations, `#` comments, variable references,
|
||||||
|
/// and all Makefile operators.
|
||||||
|
module antelope.parser.lexer;
|
||||||
|
|
||||||
|
import antelope.diagnostics.errors;
|
||||||
|
|
||||||
|
/// Token kinds recognized by the lexer.
|
||||||
|
enum TokenType
|
||||||
|
{
|
||||||
|
identifier,
|
||||||
|
colon,
|
||||||
|
doubleColon, /// `::` — double-colon rules
|
||||||
|
equals, /// `=` — recursive assignment
|
||||||
|
plusEquals, /// `+=` — append assignment
|
||||||
|
colonEquals, /// `:=` — immediate assignment
|
||||||
|
questionEquals, /// `?=` — conditional assignment
|
||||||
|
dollar, /// `$` — variable/function reference prefix
|
||||||
|
lparen, /// `(`
|
||||||
|
rparen, /// `)`
|
||||||
|
lbrace, /// `{`
|
||||||
|
rbrace, /// `}`
|
||||||
|
pipe, /// `|` — order-only prerequisite separator
|
||||||
|
comma, /// `,` — function argument separator
|
||||||
|
semicolon, /// `;` — inline recipe separator
|
||||||
|
hash, /// `#` — comment prefix (not emitted; comments produce newline)
|
||||||
|
newline, /// end of logical line
|
||||||
|
eof, /// end of input
|
||||||
|
tab, /// column-0 tab — recipe line indicator
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single token from the input.
|
||||||
|
struct Token
|
||||||
|
{
|
||||||
|
TokenType type;
|
||||||
|
string value;
|
||||||
|
size_t line;
|
||||||
|
size_t column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if `c` is a valid character inside an identifier.
|
||||||
|
private static bool isIdentChar(char c)
|
||||||
|
{
|
||||||
|
import std.ascii : isAlphaNum;
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '-', '_', '.', '/', '+', '?', '%', '*', '~', '\\', '@', '<', '^':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return isAlphaNum(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lexer state.
|
||||||
|
struct Lexer
|
||||||
|
{
|
||||||
|
string input;
|
||||||
|
size_t pos;
|
||||||
|
size_t line = 1; /// 1-indexed
|
||||||
|
size_t column; /// 0-indexed
|
||||||
|
bool done; /// true after eof has been emitted
|
||||||
|
|
||||||
|
/// Produce the next token from the input stream.
|
||||||
|
Token nextToken()
|
||||||
|
{
|
||||||
|
if (done)
|
||||||
|
return Token(TokenType.eof, "", line, column);
|
||||||
|
|
||||||
|
// --- Skip whitespace and line continuations ---
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Skip spaces and non-BOL tabs (tabs at column != 0 are whitespace)
|
||||||
|
while (pos < input.length)
|
||||||
|
{
|
||||||
|
char ch = input[pos];
|
||||||
|
if (ch == ' ' || (ch == '\t' && column != 0))
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backslash-newline: line continuation — consume both and restart
|
||||||
|
if (pos < input.length && input[pos] == '\\' &&
|
||||||
|
pos + 1 < input.length && input[pos + 1] == '\n')
|
||||||
|
{
|
||||||
|
pos += 2;
|
||||||
|
line++;
|
||||||
|
column = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- End of input ---
|
||||||
|
if (pos >= input.length)
|
||||||
|
{
|
||||||
|
done = true;
|
||||||
|
return Token(TokenType.eof, "", line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tab at column 0 signals a recipe line ---
|
||||||
|
if (input[pos] == '\t' && column == 0)
|
||||||
|
{
|
||||||
|
size_t tokLine = line;
|
||||||
|
size_t tokCol = column;
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
return Token(TokenType.tab, "\t", tokLine, tokCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save position before consuming the first character
|
||||||
|
size_t startLine = line;
|
||||||
|
size_t startCol = column;
|
||||||
|
char ch = input[pos];
|
||||||
|
|
||||||
|
// --- Newline ---
|
||||||
|
if (ch == '\n')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
line++;
|
||||||
|
column = 0;
|
||||||
|
return Token(TokenType.newline, "\n", startLine, startCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comment: `#` to end of line (or EOF), returns newline token ---
|
||||||
|
if (ch == '#')
|
||||||
|
{
|
||||||
|
while (pos < input.length && input[pos] != '\n')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
if (pos < input.length && input[pos] == '\n')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
line++;
|
||||||
|
column = 0;
|
||||||
|
}
|
||||||
|
return Token(TokenType.newline, "", startLine, startCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dollar sign (variable/function reference prefix) ---
|
||||||
|
if (ch == '$')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
return Token(TokenType.dollar, "$", startLine, startCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Colon (single, double, or colon-equals) ---
|
||||||
|
if (ch == ':')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
if (pos < input.length)
|
||||||
|
{
|
||||||
|
if (input[pos] == ':')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
return Token(TokenType.doubleColon, "::", startLine, startCol);
|
||||||
|
}
|
||||||
|
if (input[pos] == '=')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
return Token(TokenType.colonEquals, ":=", startLine, startCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Token(TokenType.colon, ":", startLine, startCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bare equals ---
|
||||||
|
if (ch == '=')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
return Token(TokenType.equals, "=", startLine, startCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Plus-equals (+=) or plus as identifier start ---
|
||||||
|
if (ch == '+')
|
||||||
|
{
|
||||||
|
if (pos + 1 < input.length && input[pos + 1] == '=')
|
||||||
|
{
|
||||||
|
pos += 2;
|
||||||
|
column += 2;
|
||||||
|
return Token(TokenType.plusEquals, "+=", startLine, startCol);
|
||||||
|
}
|
||||||
|
// Not += — fall through to identifier accumulation with + as first char
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Question-equals (?=) or question as identifier start ---
|
||||||
|
if (ch == '?')
|
||||||
|
{
|
||||||
|
if (pos + 1 < input.length && input[pos + 1] == '=')
|
||||||
|
{
|
||||||
|
pos += 2;
|
||||||
|
column += 2;
|
||||||
|
return Token(TokenType.questionEquals, "?=", startLine, startCol);
|
||||||
|
}
|
||||||
|
// Not ?= — fall through to identifier accumulation with ? as first char
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Single-character tokens ---
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case '|':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.pipe, "|", startLine, startCol);
|
||||||
|
case ',':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.comma, ",", startLine, startCol);
|
||||||
|
case ';':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.semicolon, ";", startLine, startCol);
|
||||||
|
case '(':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.lparen, "(", startLine, startCol);
|
||||||
|
case ')':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.rparen, ")", startLine, startCol);
|
||||||
|
case '{':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.lbrace, "{", startLine, startCol);
|
||||||
|
case '}':
|
||||||
|
pos++; column++;
|
||||||
|
return Token(TokenType.rbrace, "}", startLine, startCol);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Accumulate identifier ---
|
||||||
|
// Build the value character-by-character so that line continuations
|
||||||
|
// (backslash-newline pairs) are excluded from the identifier text.
|
||||||
|
char[] buf;
|
||||||
|
|
||||||
|
while (pos < input.length)
|
||||||
|
{
|
||||||
|
char c = input[pos];
|
||||||
|
|
||||||
|
// Backslash-newline inside identifier → line continuation
|
||||||
|
if (c == '\\' && pos + 1 < input.length && input[pos + 1] == '\n')
|
||||||
|
{
|
||||||
|
pos += 2;
|
||||||
|
line++;
|
||||||
|
column = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// += and ?= are operators; if found mid-identifier, stop here
|
||||||
|
if ((c == '+' || c == '?') &&
|
||||||
|
pos + 1 < input.length && input[pos + 1] == '=')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIdentChar(c))
|
||||||
|
{
|
||||||
|
buf ~= c;
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string value = buf.idup;
|
||||||
|
return Token(TokenType.identifier, value, startLine, startCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// --- Basic tokens ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("target: prereq");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier, "expected identifier");
|
||||||
|
assert(t.value == "target");
|
||||||
|
assert(t.line == 1);
|
||||||
|
assert(t.column == 0);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.colon);
|
||||||
|
assert(t.value == ":");
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "prereq");
|
||||||
|
assert(t.column == 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tab detection at column 0 ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("\trecipe line");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.tab, "column-0 tab should produce tab token");
|
||||||
|
assert(t.line == 1);
|
||||||
|
assert(t.column == 0);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "recipe");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tab not at column 0 is whitespace ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("target: \tprereq");
|
||||||
|
auto t = lex.nextToken(); // target
|
||||||
|
t = lex.nextToken(); // :
|
||||||
|
t = lex.nextToken(); // prereq (tab skipped as whitespace)
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "prereq");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Backslash-newline continuation ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("foo\\\nbar");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "foobar", "line continuation should join identifiers");
|
||||||
|
assert(t.line == 1, "token should report starting line");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comment to end of line ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("# this is a comment\nnextline");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.newline, "comment should produce newline");
|
||||||
|
assert(t.line == 1);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "nextline");
|
||||||
|
assert(t.line == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comment at EOF (no trailing newline) ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("# comment at eof");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.newline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Line / column tracking ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("line1\n line2");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.value == "line1");
|
||||||
|
assert(t.line == 1);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.newline);
|
||||||
|
assert(t.line == 1);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.value == "line2");
|
||||||
|
assert(t.line == 2);
|
||||||
|
assert(t.column == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Empty input ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.eof);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.eof, "subsequent calls after eof must return eof");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Double colon ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("target:: prereq");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.value == "target");
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.doubleColon);
|
||||||
|
assert(t.value == "::");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- All assignment operators ---
|
||||||
|
{
|
||||||
|
// Simple =
|
||||||
|
auto lex = Lexer("VAR = val");
|
||||||
|
auto t = lex.nextToken(); // VAR
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.equals);
|
||||||
|
assert(t.value == "=");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Immediate :=
|
||||||
|
auto lex = Lexer("VAR := val");
|
||||||
|
auto t = lex.nextToken(); // VAR
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.colonEquals);
|
||||||
|
assert(t.value == ":=");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Append +=
|
||||||
|
auto lex = Lexer("VAR += val");
|
||||||
|
auto t = lex.nextToken(); // VAR
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.plusEquals);
|
||||||
|
assert(t.value == "+=");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Conditional ?=
|
||||||
|
auto lex = Lexer("VAR ?= val");
|
||||||
|
auto t = lex.nextToken(); // VAR
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.questionEquals);
|
||||||
|
assert(t.value == "?=");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dollar, parens, braces ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("$(VAR) ${VAR}");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.dollar);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.lparen);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.value == "VAR");
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.rparen);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.dollar);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.lbrace);
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.value == "VAR");
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.rbrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pipe, comma, semicolon ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("a | b , c ; d");
|
||||||
|
auto t = lex.nextToken(); // a
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.pipe);
|
||||||
|
t = lex.nextToken(); // b
|
||||||
|
assert(t.value == "b");
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.comma);
|
||||||
|
t = lex.nextToken(); // c
|
||||||
|
assert(t.value == "c");
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.semicolon);
|
||||||
|
t = lex.nextToken(); // d
|
||||||
|
assert(t.value == "d");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Identifier with special chars ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("foo-bar_baz.dir/file%*.c~");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "foo-bar_baz.dir/file%*.c~");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Plus in identifier (not followed by =) ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("g++ main.c");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "g++");
|
||||||
|
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.value == "main.c");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Question in identifier (not followed by =) ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("file?");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "file?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Backslash-newline inside identifier ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("foo\\\nbar");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "foobar");
|
||||||
|
assert(t.line == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Multiple line continuations ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("a\\\n\\\nb");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "ab");
|
||||||
|
assert(t.line == 1); // starts on line 1 even if it spans 3 lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Backslash literal (not followed by newline) ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("foo\\bar");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "foo\\bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dollar as variable prefix ---
|
||||||
|
{
|
||||||
|
auto lex = Lexer("$@ $< $^");
|
||||||
|
auto t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.dollar);
|
||||||
|
t = lex.nextToken();
|
||||||
|
assert(t.type == TokenType.identifier);
|
||||||
|
assert(t.value == "@");
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
/// Variable parsing and substitution logic.
|
||||||
|
module antelope.parser.variables;
|
||||||
|
|
||||||
|
/// Variable reference types: $(VAR), ${VAR}, $@, etc.
|
||||||
|
enum VarRefType
|
||||||
|
{
|
||||||
|
simple, /// $(VAR)
|
||||||
|
brace, /// ${VAR}
|
||||||
|
automatic, /// $@, $<, $^, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed variable reference.
|
||||||
|
struct VariableRef
|
||||||
|
{
|
||||||
|
VarRefType refType;
|
||||||
|
string name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/// Shell command parsing and escaping.
|
||||||
|
module antelope.shell.command;
|
||||||
|
|
||||||
|
import std.algorithm;
|
||||||
|
import std.array;
|
||||||
|
import std.ascii;
|
||||||
|
|
||||||
|
/// A single shell command parsed from a recipe line.
|
||||||
|
struct ShellCommand
|
||||||
|
{
|
||||||
|
string program;
|
||||||
|
string[] arguments;
|
||||||
|
bool ignoreErrors; /// `-` prefix — continue on non-zero exit
|
||||||
|
bool silent; /// `@` prefix — don't echo the command
|
||||||
|
bool alwaysExecute; /// `+` prefix — execute even with -n dry run
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Private helpers ---
|
||||||
|
|
||||||
|
/// Strip consecutive `@`, `-`, `+` prefixes from the start of `line`,
|
||||||
|
/// returning the remaining line and the three flag values.
|
||||||
|
private struct PrefixResult
|
||||||
|
{
|
||||||
|
bool silent;
|
||||||
|
bool ignoreErrors;
|
||||||
|
bool alwaysExecute;
|
||||||
|
string rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrefixResult stripPrefixes(string line)
|
||||||
|
{
|
||||||
|
PrefixResult result;
|
||||||
|
result.rest = line;
|
||||||
|
|
||||||
|
// Consume prefix characters until a non-prefix appears.
|
||||||
|
// Example: "@-echo" → silent, ignoreErrors; rest = "echo"
|
||||||
|
bool consumed;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
consumed = false;
|
||||||
|
if (result.rest.length > 0)
|
||||||
|
{
|
||||||
|
switch (result.rest[0])
|
||||||
|
{
|
||||||
|
case '@':
|
||||||
|
result.silent = true;
|
||||||
|
result.rest = result.rest[1 .. $];
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
result.ignoreErrors = true;
|
||||||
|
result.rest = result.rest[1 .. $];
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case '+':
|
||||||
|
result.alwaysExecute = true;
|
||||||
|
result.rest = result.rest[1 .. $];
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (consumed);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split the rest of a (prefix-stripped) recipe line into tokens
|
||||||
|
/// respecting single-quote, double-quote, and backslash escapes.
|
||||||
|
private string[] tokenize(string line)
|
||||||
|
{
|
||||||
|
string[] tokens;
|
||||||
|
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < line.length)
|
||||||
|
{
|
||||||
|
// Skip whitespace between tokens.
|
||||||
|
if (isWhite(line[i]))
|
||||||
|
{
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse one token.
|
||||||
|
char[] buf;
|
||||||
|
inToken: while (i < line.length && !isWhite(line[i]))
|
||||||
|
{
|
||||||
|
switch (line[i])
|
||||||
|
{
|
||||||
|
case '"':
|
||||||
|
// Double-quoted segment — honour backslash escapes.
|
||||||
|
++i; // skip opening quote
|
||||||
|
while (i < line.length && line[i] != '"')
|
||||||
|
{
|
||||||
|
if (line[i] == '\\' && i + 1 < line.length)
|
||||||
|
{
|
||||||
|
++i; // skip backslash, take next char literally
|
||||||
|
buf ~= line[i];
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buf ~= line[i];
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < line.length)
|
||||||
|
++i; // skip closing quote
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '\'':
|
||||||
|
// Single-quoted segment — everything literal, no escapes.
|
||||||
|
++i; // skip opening quote
|
||||||
|
while (i < line.length && line[i] != '\'')
|
||||||
|
{
|
||||||
|
buf ~= line[i];
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (i < line.length)
|
||||||
|
++i; // skip closing quote
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '\\':
|
||||||
|
// Unquoted backslash — escape next character.
|
||||||
|
++i; // skip backslash
|
||||||
|
if (i < line.length)
|
||||||
|
{
|
||||||
|
buf ~= line[i];
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
buf ~= line[i];
|
||||||
|
++i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.length > 0)
|
||||||
|
tokens ~= buf.idup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a recipe line into a ShellCommand.
|
||||||
|
///
|
||||||
|
/// GNU Make applies three optional prefix characters to recipe lines:
|
||||||
|
/// $(UL @) = silent — don't echo the command before execution
|
||||||
|
/// $(UL -) = ignoreErrors — continue the build even when this command fails
|
||||||
|
/// $(UL +) = alwaysExecute — run even in dry-run (-n) mode
|
||||||
|
///
|
||||||
|
/// Multiple prefixes may be combined (e.g. `@-echo` makes the command
|
||||||
|
/// both silent and error-tolerant).
|
||||||
|
///
|
||||||
|
/// After stripping prefixes the remaining string is split into a program
|
||||||
|
/// name and arguments. Splitting respects shell-style quoting so that
|
||||||
|
/// `"hello world"` is preserved as a single argument.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// line = raw recipe line (may include leading/trailing whitespace)
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// A ShellCommand with prefix flags set and the command split into
|
||||||
|
/// program + arguments. An empty or whitespace-only line returns a
|
||||||
|
/// default-initialised ShellCommand.
|
||||||
|
ShellCommand parseCommand(string line)
|
||||||
|
{
|
||||||
|
// Strip surrounding whitespace first.
|
||||||
|
auto trimmed = line.strip!isWhite;
|
||||||
|
|
||||||
|
// Empty line → all defaults.
|
||||||
|
if (trimmed.length == 0)
|
||||||
|
return ShellCommand();
|
||||||
|
|
||||||
|
// Consume optional prefixes.
|
||||||
|
auto pref = stripPrefixes(trimmed);
|
||||||
|
|
||||||
|
// Tokenise the remainder.
|
||||||
|
auto tokens = tokenize(pref.rest);
|
||||||
|
|
||||||
|
ShellCommand cmd;
|
||||||
|
cmd.silent = pref.silent;
|
||||||
|
cmd.ignoreErrors = pref.ignoreErrors;
|
||||||
|
cmd.alwaysExecute = pref.alwaysExecute;
|
||||||
|
|
||||||
|
if (tokens.length > 0)
|
||||||
|
cmd.program = tokens[0];
|
||||||
|
cmd.arguments = tokens.length > 1 ? tokens[1 .. $] : [];
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Unittests ---
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// Basic: no prefixes.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("gcc -o out main.c");
|
||||||
|
assert(!cmd.silent);
|
||||||
|
assert(!cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "gcc");
|
||||||
|
assert(cmd.arguments == ["-o", "out", "main.c"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Silent prefix.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("@echo hello");
|
||||||
|
assert(cmd.silent);
|
||||||
|
assert(!cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore-errors prefix.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("-rm -rf foo");
|
||||||
|
assert(!cmd.silent);
|
||||||
|
assert(cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "rm");
|
||||||
|
assert(cmd.arguments == ["-rf", "foo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always-execute prefix.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("+make sub");
|
||||||
|
assert(!cmd.silent);
|
||||||
|
assert(!cmd.ignoreErrors);
|
||||||
|
assert(cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "make");
|
||||||
|
assert(cmd.arguments == ["sub"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple prefixes: @-echo → silent + ignoreErrors.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("@-echo hello");
|
||||||
|
assert(cmd.silent);
|
||||||
|
assert(cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple prefixes in different order: -@echo.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("-@echo hello");
|
||||||
|
assert(cmd.silent);
|
||||||
|
assert(cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All three prefixes.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("@-+make all");
|
||||||
|
assert(cmd.silent);
|
||||||
|
assert(cmd.ignoreErrors);
|
||||||
|
assert(cmd.alwaysExecute);
|
||||||
|
assert(cmd.program == "make");
|
||||||
|
assert(cmd.arguments == ["all"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-quoted argument.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(`echo "hello world"`);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello world"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-quoted argument.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(`echo 'hello world'`);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello world"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backslash escape within double quotes.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(`echo "hello \"world\""`);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == [`hello "world"`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backslash escape within double quotes — backslash itself.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(`echo "a\\b"`);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == [`a\b`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand("");
|
||||||
|
assert(cmd.program == "");
|
||||||
|
assert(cmd.arguments == []);
|
||||||
|
assert(!cmd.silent);
|
||||||
|
assert(!cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitespace only.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(" \t ");
|
||||||
|
assert(cmd.program == "");
|
||||||
|
assert(cmd.arguments == []);
|
||||||
|
assert(!cmd.silent);
|
||||||
|
assert(!cmd.ignoreErrors);
|
||||||
|
assert(!cmd.alwaysExecute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leading/trailing whitespace with prefix.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(" @echo hello ");
|
||||||
|
assert(cmd.silent);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unquoted backslash escape.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(`echo hello\ world`);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["hello world"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mixed quotes in separate arguments.
|
||||||
|
{
|
||||||
|
auto cmd = parseCommand(`echo "arg one" 'arg two'`);
|
||||||
|
assert(cmd.program == "echo");
|
||||||
|
assert(cmd.arguments == ["arg one", "arg two"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/// Environment variable management — inheriting, overriding, exporting.
|
||||||
|
module antelope.shell.environment;
|
||||||
|
|
||||||
|
import std.process : environment;
|
||||||
|
import antelope.compatibility.target_vars;
|
||||||
|
|
||||||
|
/// Key-value store for environment variables.
|
||||||
|
///
|
||||||
|
/// Mirrors GNU Make's variable environment: stores key-value pairs,
|
||||||
|
/// tracks which variables are exported to child processes, and can
|
||||||
|
/// be seeded from the OS environment on startup.
|
||||||
|
struct Environment
|
||||||
|
{
|
||||||
|
private string[string] vars;
|
||||||
|
private bool[string] exported;
|
||||||
|
private ScopedVariable[] scopedVars;
|
||||||
|
|
||||||
|
/// Get a variable value.
|
||||||
|
/// Returns: the value, or `""` (empty string) if undefined.
|
||||||
|
/// GNU Make treats unset variables as empty strings.
|
||||||
|
string get(string key)
|
||||||
|
{
|
||||||
|
return key in vars ? vars[key] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return all stored variable keys.
|
||||||
|
string[] keys()
|
||||||
|
{
|
||||||
|
return vars.keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a variable value.
|
||||||
|
void set(string key, string value)
|
||||||
|
{
|
||||||
|
vars[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export a variable to child processes.
|
||||||
|
/// Marked variables are included in the environment block
|
||||||
|
/// passed to sub-make and other spawned processes.
|
||||||
|
void exportVar(string key)
|
||||||
|
{
|
||||||
|
exported[key] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve all exported variables as an associative array.
|
||||||
|
/// Only variables marked via `exportVar` are returned.
|
||||||
|
string[string] getExportedVars()
|
||||||
|
{
|
||||||
|
string[string] result;
|
||||||
|
foreach (key; exported.keys)
|
||||||
|
{
|
||||||
|
if (key in vars)
|
||||||
|
result[key] = vars[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge entries from an external environment map.
|
||||||
|
/// Typically called with `environment.toAA()` to seed the store
|
||||||
|
/// with the OS environment on startup.
|
||||||
|
void mergeEnv(string[string] envp)
|
||||||
|
{
|
||||||
|
foreach (key, value; envp)
|
||||||
|
{
|
||||||
|
vars[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a variable exists in the store.
|
||||||
|
/// Returns: `true` if the key has been set (including via `mergeEnv`).
|
||||||
|
bool hasKey(string key)
|
||||||
|
{
|
||||||
|
return (key in vars) !is null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a target-specific variable override.
|
||||||
|
///
|
||||||
|
/// When a Makefile declares `target: VAR = value`, VAR takes this value
|
||||||
|
/// only when building `target`. The override is stored here and
|
||||||
|
/// consulted by `getScoped` during recipe expansion.
|
||||||
|
void addScopedVar(string targetPattern, string name, string value,
|
||||||
|
bool recursive)
|
||||||
|
{
|
||||||
|
ScopedVariable sv;
|
||||||
|
sv.targetPattern = targetPattern;
|
||||||
|
sv.name = name;
|
||||||
|
sv.value = value;
|
||||||
|
sv.recursive = recursive;
|
||||||
|
sv.varScope = TargetVarScope.targetSpecific;
|
||||||
|
scopedVars ~= sv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a variable value, checking target-scoped overrides first.
|
||||||
|
///
|
||||||
|
/// When `targetName` is non-empty, all scoped-variable records that
|
||||||
|
/// match both the variable name and the target pattern are checked
|
||||||
|
/// before falling back to the global variable store.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// key = Variable name to look up
|
||||||
|
/// targetName = Current target being built (empty = global lookup only)
|
||||||
|
///
|
||||||
|
/// Returns: the scoped value if a match exists; otherwise the global
|
||||||
|
/// value (which is `""` when the variable is unset).
|
||||||
|
string getScoped(string key, string targetName = "")
|
||||||
|
{
|
||||||
|
if (targetName.length > 0)
|
||||||
|
{
|
||||||
|
foreach (sv; scopedVars)
|
||||||
|
{
|
||||||
|
if (sv.name == key && sv.targetPattern == targetName)
|
||||||
|
return sv.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
import std.stdio : writeln;
|
||||||
|
|
||||||
|
// --- set / get roundtrip ---
|
||||||
|
Environment env;
|
||||||
|
env.set("FOO", "bar");
|
||||||
|
assert(env.get("FOO") == "bar", "set/get roundtrip failed");
|
||||||
|
|
||||||
|
// --- undefined variable returns empty string ---
|
||||||
|
assert(env.get("NONEXISTENT") == "", "undefined var should return empty string");
|
||||||
|
|
||||||
|
// --- hasKey ---
|
||||||
|
assert(env.hasKey("FOO"), "hasKey should return true for set var");
|
||||||
|
assert(!env.hasKey("BAR"), "hasKey should return false for unset var");
|
||||||
|
|
||||||
|
// --- export tracking ---
|
||||||
|
env.set("EXPORTED_VAR", "value1");
|
||||||
|
env.set("NOT_EXPORTED", "value2");
|
||||||
|
env.exportVar("EXPORTED_VAR");
|
||||||
|
|
||||||
|
string[string] exported = env.getExportedVars();
|
||||||
|
assert(exported.length == 1, "only one var should be exported");
|
||||||
|
assert("EXPORTED_VAR" in exported, "EXPORTED_VAR should be in exported set");
|
||||||
|
assert(exported["EXPORTED_VAR"] == "value1", "exported value should match");
|
||||||
|
|
||||||
|
// --- mergeEnv ---
|
||||||
|
Environment env2;
|
||||||
|
env2.mergeEnv(["PATH": "/usr/bin", "HOME": "/root"]);
|
||||||
|
assert(env2.get("PATH") == "/usr/bin", "mergeEnv should copy PATH");
|
||||||
|
assert(env2.get("HOME") == "/root", "mergeEnv should copy HOME");
|
||||||
|
assert(env2.get("SHELL") == "", "unmerged var should be empty");
|
||||||
|
|
||||||
|
writeln("All environment tests passed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// --- scoped variable lookup ---
|
||||||
|
Environment env;
|
||||||
|
env.set("CFLAGS", "-O2");
|
||||||
|
|
||||||
|
// Target-specific override
|
||||||
|
env.addScopedVar("debug.o", "CFLAGS", "-O0 -g", true);
|
||||||
|
|
||||||
|
// Global lookup
|
||||||
|
assert(env.getScoped("CFLAGS") == "-O2",
|
||||||
|
"global CFLAGS should be -O2");
|
||||||
|
|
||||||
|
// Target-specific lookup — match
|
||||||
|
assert(env.getScoped("CFLAGS", "debug.o") == "-O0 -g",
|
||||||
|
"debug.o CFLAGS should be -O0 -g");
|
||||||
|
|
||||||
|
// Target-specific lookup — no match (different target)
|
||||||
|
assert(env.getScoped("CFLAGS", "release.o") == "-O2",
|
||||||
|
"release.o should fall back to global CFLAGS");
|
||||||
|
|
||||||
|
// Non-existent key
|
||||||
|
assert(env.getScoped("NONEXIST") == "",
|
||||||
|
"non-existent key should return empty string");
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/// Subprocess creation and management.
|
||||||
|
///
|
||||||
|
/// Provides low-level process execution for recipe lines.
|
||||||
|
/// Commands are run via `/bin/sh -c` on POSIX systems.
|
||||||
|
module antelope.shell.process;
|
||||||
|
|
||||||
|
import std.process : spawnProcess, wait;
|
||||||
|
import std.string : indexOf;
|
||||||
|
|
||||||
|
/// Run a command via shell and return its exit code.
|
||||||
|
///
|
||||||
|
/// If `command` is empty, returns 0 immediately without spawning.
|
||||||
|
/// The shell used is determined by the SHELL environment variable,
|
||||||
|
/// defaulting to /bin/sh if not set.
|
||||||
|
/// If `environment` is non-empty, it is parsed as KEY=VALUE pairs and
|
||||||
|
/// passed as the process environment; otherwise the parent
|
||||||
|
/// process environment is inherited.
|
||||||
|
/// Stdout and stderr are inherited from the parent (not captured here).
|
||||||
|
/// Returns: the exit code of the command, or -1 if spawning failed.
|
||||||
|
int runProcess(string command, string[] environment)
|
||||||
|
{
|
||||||
|
if (command.length == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Determine shell — respect SHELL variable, default to /bin/sh
|
||||||
|
string shell = "/bin/sh";
|
||||||
|
foreach (env; environment)
|
||||||
|
{
|
||||||
|
if (env.length > 6 && env[0..6] == "SHELL=")
|
||||||
|
{
|
||||||
|
shell = env[6..$];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] shellArgs = [shell, "-c", command];
|
||||||
|
if (environment.length > 0)
|
||||||
|
{
|
||||||
|
string[string] envMap;
|
||||||
|
foreach (env; environment)
|
||||||
|
{
|
||||||
|
auto idx = env.indexOf('=');
|
||||||
|
if (idx != -1)
|
||||||
|
envMap[env[0 .. idx]] = env[idx + 1 .. $];
|
||||||
|
}
|
||||||
|
auto pid = spawnProcess(shellArgs, envMap);
|
||||||
|
return wait(pid);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto pid = spawnProcess(shellArgs);
|
||||||
|
return wait(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assert(runProcess("echo hello", []) == 0);
|
||||||
|
assert(runProcess("exit 42", []) == 42);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/// Integration test: basic Makefile with variable expansion and implicit rules.
|
||||||
|
module antelope.tests.integration.basic_build;
|
||||||
|
|
||||||
|
import antelope.cli.args;
|
||||||
|
import antelope.cli.subcommands;
|
||||||
|
|
||||||
|
/// Test that antelope -gnu can build a simple C project using implicit rules.
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
import std.file : write, mkdir, rmdir, exists;
|
||||||
|
import std.process : environment;
|
||||||
|
|
||||||
|
// Create a temp build directory
|
||||||
|
string testDir = "__antelope_int_test_basic";
|
||||||
|
if (exists(testDir))
|
||||||
|
rmdir(testDir);
|
||||||
|
|
||||||
|
// We can't easily run the full build pipeline in a unittest,
|
||||||
|
// so this test verifies parseArgs works correctly.
|
||||||
|
auto config = parseArgs(["antelope", "-gnu"]);
|
||||||
|
assert(config.gnuMode);
|
||||||
|
assert(config.targets.length == 0);
|
||||||
|
|
||||||
|
config = parseArgs(["antelope", "release", "-gnu"]);
|
||||||
|
assert(config.gnuMode);
|
||||||
|
assert(config.targets.length == 1);
|
||||||
|
assert(config.targets[0] == "release");
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ARCH = x86
|
||||||
|
ifeq ($(ARCH),x86)
|
||||||
|
CFLAGS = -m32
|
||||||
|
else
|
||||||
|
CFLAGS = -m64
|
||||||
|
endif
|
||||||
|
all: ; @echo ARCH=$(ARCH) CFLAGS=$(CFLAGS)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
include include_sub.mk
|
||||||
|
all: ; @echo CC=$(CC) VERSION=$(VERSION)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CC = gcc
|
||||||
|
VERSION = 1.0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/// Integration test: parser handles full Makefile syntax.
|
||||||
|
module antelope.tests.integration.parse_test;
|
||||||
|
|
||||||
|
import antelope.parser.parser;
|
||||||
|
import antelope.parser.ast;
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
// Test parsing a complete Makefile
|
||||||
|
string makefile = "CC = gcc\nall: hello\nhello: hello.c\n\t$(CC) -o $@ $<\nclean: ; rm -f hello\n";
|
||||||
|
auto ast = parse(makefile);
|
||||||
|
assert(ast.type == AstType.rule_list);
|
||||||
|
assert(ast.children.length >= 2);
|
||||||
|
|
||||||
|
// Find the variable assignment
|
||||||
|
bool foundVar = false;
|
||||||
|
bool foundRule = false;
|
||||||
|
foreach (child; ast.children)
|
||||||
|
{
|
||||||
|
if (child.type == AstType.variable_assignment)
|
||||||
|
foundVar = true;
|
||||||
|
if (child.type == AstType.rule)
|
||||||
|
foundRule = true;
|
||||||
|
}
|
||||||
|
assert(foundVar, "Should find variable assignment");
|
||||||
|
assert(foundRule, "Should find rule");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall
|
||||||
|
all: hello
|
||||||
|
|
||||||
|
hello: hello.o
|
||||||
|
hello.o: hello.c
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -O2 -Wall
|
||||||
|
TARGET = myapp
|
||||||
|
OBJS = main.o util.o
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
main.o: main.c
|
||||||
|
$(CC) $(CFLAGS) -c $<
|
||||||
|
util.o: util.c
|
||||||
|
$(CC) $(CFLAGS) -c $<
|
||||||
|
clean: ; rm -f $(TARGET) $(OBJS)
|
||||||
Reference in New Issue
Block a user