first commit :)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user