first commit :)

This commit is contained in:
2026-07-27 23:01:30 -04:00
commit 5f891dab16
63 changed files with 9491 additions and 0 deletions
+32
View File
@@ -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);
}
+213
View File
@@ -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");
}
+62
View File
@@ -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);
}
+301
View File
@@ -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);
}
+132
View File
@@ -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);
}
+20
View File
@@ -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)
}
+465
View File
@@ -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"]);
}
}
+29
View File
@@ -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");
}
+356
View File
@@ -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;
}
+12
View File
@@ -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");
}
+28
View File
@@ -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;
}
+30
View File
@@ -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;
}
+90
View File
@@ -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;
}
+57
View File
@@ -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 "";
}
+106
View File
@@ -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;
}
+24
View File
@@ -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;
}
+100
View File
@@ -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);
}
+76
View File
@@ -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;
}
+205
View File
@@ -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
+881
View File
@@ -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
+143
View File
@@ -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);
}
+44
View File
@@ -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);
}
+115
View File
@@ -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"]));
}
+22
View File
@@ -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;
}
+19
View File
@@ -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,
}
+45
View File
@@ -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;
}
+536
View File
@@ -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
+17
View File
@@ -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;
}
+340
View File
@@ -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"]);
}
}
+182
View File
@@ -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");
}
+67
View File
@@ -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);
}