0.2 Release push

This commit is contained in:
2026-07-28 19:01:06 -04:00
parent fb7fdf330f
commit 188987d54a
13 changed files with 333 additions and 36 deletions
+12
View File
@@ -0,0 +1,12 @@
0.0 musl ✓
0.1 dwm ✓
0.2 Make itself ✓
0.3 Git
0.4 BusyBox
0.5 Binutils
0.6 QEMU
0.7 GDC
0.8 Linux kernel
0.9 OpenSSH
0.9.x Coreutils
1.0 GCC
+1
View File
@@ -17,4 +17,5 @@ struct Target
string[] prerequisites; /// Normal prerequisites (trigger rebuild)
string[] recipe; /// Shell commands to build this target
string[] orderOnlyPrereqs; /// Order-only prerequisites (| — must exist, no rebuild trigger)
string stem; /// Pattern/suffix rule stem for $* expansion
}
+15 -1
View File
@@ -6,7 +6,7 @@ module antelope.cli.args;
import std.algorithm.searching : startsWith;
import std.conv : to;
import std.string : strip;
import std.string : strip, indexOf;
import std.ascii : isDigit;
/// Available subcommands.
@@ -54,6 +54,10 @@ struct CliConfig
/// Show version (--version).
bool showVersion;
/// Variable overrides from the command line (VAR=value style).
/// GNU Make passes these with highest precedence.
string[string] varOverrides;
}
// --- Helpers ---
@@ -229,6 +233,16 @@ CliConfig parseArgs(string[] args)
continue;
}
// Variable override: VAR=value (GNU Make command-line var)
auto eqPos = indexOf(arg, '=');
if (eqPos > 0)
{
string varName = arg[0 .. eqPos];
string varValue = arg[eqPos + 1 .. $];
config.varOverrides[varName] = varValue;
continue;
}
// Everything else is a target
config.targets ~= arg;
}
+47 -1
View File
@@ -117,6 +117,35 @@ int runBuild(CliConfig config)
env.set("MAKECMDGOALS", config.targets.join(" "));
}
// Parse MAKEFLAGS for variable overrides propagated from parent
// antelope processes via recursive $(MAKE). These are serialized
// in MAKEFLAGS as "VAR=value" tokens.
if (env.hasKey("MAKEFLAGS"))
{
import std.string : split, indexOf;
import std.algorithm : startsWith;
auto mkFlags = env.get("MAKEFLAGS");
foreach (word; mkFlags.split(" "))
{
auto eqPos = indexOf(word, '=');
if (eqPos > 0 && !word.startsWith("-"))
{
string varName = word[0 .. eqPos];
string varValue = word[eqPos + 1 .. $];
config.varOverrides[varName] = varValue;
}
}
}
// Apply command-line variable overrides (VAR=value args) BEFORE
// Makefile evaluation so they take precedence over Makefile
// assignments. GNU Make semantics: command-line vars beat
// everything except `override` directives.
foreach (varName, varValue; config.varOverrides)
{
env.setCmdOverride(varName, varValue);
}
// Evaluate AST → populate env + build graph
auto graph = new DependencyGraph();
@@ -228,11 +257,28 @@ int runBuild(CliConfig config)
foreach (targetName; buildTargets)
{
if (!graph.hasTarget(targetName))
{
// Target not explicitly defined — try to create it from
// implicit rules (suffix rules, pattern rules). Autotools
// Makefiles invoke $(MAKE) with targets like "be.gmo" that
// are only defined via suffix rules (e.g., .po.gmo:).
import antelope.build.target;
Target stub;
stub.name = targetName;
stub.kind = TargetKind.file;
graph.addTarget(stub);
import antelope.evaluator.evaluator : resolveImplicitRules;
resolveImplicitRules(*graph, env);
if (!graph.hasTarget(targetName) ||
graph.findTarget(targetName).recipe.length == 0)
{
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);
@@ -263,7 +309,7 @@ int runBuild(CliConfig config)
{
// Expand variables in the recipe
string expanded = expand(recipeLine, env, t.name,
t.prerequisites);
t.prerequisites, t.stem);
// Print the command unless silent (@ prefix)
import std.string : stripLeft;
+6
View File
@@ -40,11 +40,17 @@ struct SubMakeConfig
/// -n — dry run
/// -P — POSIX conformance mode
/// -d — debug output
/// VAR=val — command-line variable overrides
///
/// Returns: a space-delimited MAKEFLAGS string, or "" if no flags are active.
string serializeMakeFlags(CliConfig config)
{
string flags;
// Variable overrides first (parsed back by parseArgs in sub-make)
foreach (varName, varValue; config.varOverrides)
flags ~= " " ~ varName ~ "=" ~ varValue;
if (config.jobs > 1)
flags ~= " -j" ~ config.jobs.to!string;
if (config.dryRun)
+159 -25
View File
@@ -129,6 +129,7 @@ size_t resolveImplicitRules(ref DependencyGraph graph, Environment* env = null)
if (prereqSatisfiable(match.resolvedPrereq, graph))
{
tp.recipe = match.rule.recipe;
tp.stem = match.stem; // for $* expansion in recipes
if (match.resolvedPrereq.length > 0)
{
bool alreadyPresent;
@@ -171,12 +172,103 @@ size_t resolveImplicitRules(ref DependencyGraph graph, Environment* env = null)
if (userMatches.length > 0)
{
auto um = userMatches[$ - 1];
// Skip pattern rules with no recipe — GNU Make treats these
// as "cancellation" rules that remove implicit rules rather
// than providing new ones. Applying an empty recipe would
// leave the target unresolved (recipe.length==0), causing
// infinite re-resolution every pass.
if (um.rule.recipe.length > 0)
{
resolveUserPatternRule(tp, um, graph, env);
resolved++;
applied = true;
}
}
// If still no match, try suffix rules (e.g., .c.o:).
// GNU Make suffix rules provide recipes for targets based on
// the file extensions of the target and its prerequisites.
if (!applied)
{
import std.string : lastIndexOf;
import std.file : exists;
auto tDot = lastIndexOf(tp.name, '.');
if (tDot >= 0)
{
string tSuffix = tp.name[tDot .. $]; // e.g. ".o"
// First pass: match against existing prerequisites.
if (tp.prerequisites.length > 0)
{
foreach (prereq; tp.prerequisites)
{
auto pDot = lastIndexOf(prereq, '.');
if (pDot >= 0)
{
string pSuffix = prereq[pDot .. $];
string ruleName = pSuffix ~ tSuffix;
auto sr = graph.findTarget(ruleName);
// Only apply if the source prerequisite actually
// exists on disk (matching GNU Make semantics).
if (sr !is null && sr.recipe.length > 0 && exists(prereq))
{
tp.recipe = sr.recipe;
// Stem for $*: everything before the target suffix
tp.stem = tp.name[0 .. tDot];
resolved++;
applied = true;
break;
}
}
}
}
// Second pass: no existing prereqs — try to discover
// a source file by matching suffix rules. For target
// "be.gmo", find suffix rules ending in ".gmo" (e.g.,
// ".po.gmo"), construct the source name "be.po", and
// check if it exists on disk.
if (!applied)
{
string stem = tp.name[0 .. tDot]; // e.g. "be"
// Scan all graph targets for suffix rules matching tSuffix
foreach (ref gt; graph.targets)
{
if (gt.name.length > tSuffix.length &&
gt.name[$ - tSuffix.length .. $] == tSuffix &&
gt.name[0] == '.')
{
// gt.name is e.g. ".po.gmo" — source suffix is
// everything before tSuffix: ".po"
string srcSuffix = gt.name[0 .. $ - tSuffix.length];
if (srcSuffix.length == 0 || srcSuffix[0] != '.')
continue;
string srcFile = stem ~ srcSuffix; // e.g. "be.po"
if (exists(srcFile) && gt.recipe.length > 0)
{
tp.recipe = gt.recipe;
tp.stem = stem; // for $* expansion
// Add source as a prerequisite
bool alreadyPresent;
foreach (p; tp.prerequisites)
if (p == srcFile) { alreadyPresent = true; break; }
if (!alreadyPresent)
{
string[] np = [srcFile];
np ~= tp.prerequisites;
tp.prerequisites = np;
}
resolved++;
applied = true;
break;
}
}
}
}
}
}
}
}
return resolved;
@@ -193,6 +285,7 @@ private void resolveUserPatternRule(Target* tp, PatternMatch match,
ref DependencyGraph graph, Environment* env)
{
tp.recipe = match.rule.recipe;
tp.stem = match.stem; // for $* expansion in recipes
// Clear existing prereqs and set from pattern match
tp.prerequisites = [];
foreach (prereq; match.resolvedPrereqs)
@@ -337,7 +430,11 @@ private void handleRule(AstNode node, Environment* env, DependencyGraph* graph)
foreach (ref p; prereqs)
{
string expanded = expand(p, env);
foreach (word; expanded.split(" "))
// Split on ALL whitespace (not just spaces). Autotools
// Makefiles use tab-indented line continuations inside
// variable values, so a naive space-split leaves leading
// tabs that prevent prerequisite names from matching.
foreach (word; expanded.split())
{
if (word.length > 0)
expandedPrereqs ~= word;
@@ -387,12 +484,35 @@ private void handleRule(AstNode node, Environment* env, DependencyGraph* graph)
t.orderOnlyPrereqs = prereqSplit.orderOnly;
t.recipe = recipe;
// Materialize missing prerequisites as stub Targets in the graph.
// Without this, prerequisites that have no explicit rule (e.g., foo.o
// when only "all: foo.o" appears) never become graph targets, so
// resolveImplicitRules() can't give them recipes and the dependency
// resolver treats them as external/always-satisfied.
// Skip pattern prereqs containing '%' — they're templates, not real files.
if (graph)
{
import std.file : exists;
import std.string : indexOf;
foreach (prereq; prereqSplit.normal ~ prereqSplit.orderOnly)
{
if (indexOf(prereq, '%') >= 0) continue; // pattern prereq, not a real file
if (!graph.hasTarget(prereq) && !exists(prereq))
{
Target stub;
stub.name = prereq;
stub.kind = TargetKind.file;
graph.addTarget(stub);
}
}
}
// Handle multi-target rules: when target expands to multiple words,
// create separate targets for each (same prereqs + recipe).
import std.string;
import std.algorithm : filter;
import std.array : array;
auto expandedTargets = targetName.split(" ").filter!(s => s.length > 0).array;
auto expandedTargets = targetName.split().filter!(s => s.length > 0).array;
if (expandedTargets.length > 1)
{
foreach (tn; expandedTargets)
@@ -561,7 +681,7 @@ private void handleDirective(AstNode node, Environment* env, DependencyGraph* gr
import std.string : split;
VPathEntry entry;
entry.pattern = pattern;
foreach (dir; dirs.split(" "))
foreach (dir; dirs.split())
if (dir.length > 0) entry.directories ~= dir;
// Store in env for later use by the resolver
if (env !is null)
@@ -667,14 +787,21 @@ private void handleInclude(string dirName, string data, ptrdiff_t space,
{
if (space < 0) return;
string rawPath = data[space + 1 .. $].strip;
string rawPath = data[space + 1 .. $];
// Strip inline comment (# to end of logical line).
// Autotools-generated Makefiles append " # am--include-marker"
// as a comment after include directives.
auto hashPos = indexOf(rawPath, '#');
if (hashPos >= 0)
rawPath = rawPath[0 .. hashPos];
rawPath = rawPath.strip;
if (rawPath.length == 0) return;
// Expand variable references in the include path (e.g., $(DEP_FILES)
// commonly expands to "./.deps/a.Po ./.deps/b.Po" in autotools projects).
import antelope.evaluator.expansion;
import std.string : split;
auto paths = expand(rawPath, env).split(" ");
auto paths = expand(rawPath, env).split();
foreach (path; paths)
{
@@ -812,20 +939,23 @@ unittest
evaluate(root, &env, &graph);
assert(graph.targets.length == 1,
"graph should contain exactly 1 target");
assert(graph.targets[0].name == "program",
"target name should be 'program', got: " ~ graph.targets[0].name);
assert(graph.targets[0].prerequisites.length == 2,
// With prerequisite materialization, main.o + util.o become stub targets
assert(graph.targets.length == 3,
"graph should contain program + 2 prereq stubs");
auto prog = graph.findTarget("program");
assert(prog !is null, "program should be in the graph");
assert(prog.name == "program",
"target name should be 'program', got: " ~ prog.name);
assert(prog.prerequisites.length == 2,
"target should have 2 prerequisites, got: " ~
to!string(graph.targets[0].prerequisites.length));
assert(graph.targets[0].prerequisites[0] == "main.o",
to!string(prog.prerequisites.length));
assert(prog.prerequisites[0] == "main.o",
"first prereq should be 'main.o', got: " ~
graph.targets[0].prerequisites[0]);
assert(graph.targets[0].prerequisites[1] == "util.o",
prog.prerequisites[0]);
assert(prog.prerequisites[1] == "util.o",
"second prereq should be 'util.o', got: " ~
graph.targets[0].prerequisites[1]);
assert(graph.targets[0].recipe.length == 1,
prog.prerequisites[1]);
assert(prog.recipe.length == 1,
"target should have 1 recipe line");
}
@@ -1130,9 +1260,11 @@ unittest
assert(env.get("CC") == "gcc");
assert(env.get("CFLAGS") == "-Wall");
assert(graph.targets.length == 2);
assert(graph.targets[0].name == "foo.o");
assert(graph.targets[1].name == "bar.o");
// foo.o: foo.c → foo.c stub, bar.o: → no prereqs
assert(graph.targets.length == 3);
assert(graph.hasTarget("foo.o"), "foo.o should be in graph");
assert(graph.hasTarget("bar.o"), "bar.o should be in graph");
assert(graph.hasTarget("foo.c"), "foo.c stub should be in graph");
}
///
@@ -1395,13 +1527,15 @@ unittest
evaluate(root, &env, &graph);
// Target should exist with only real prereqs.
// Target should exist with only real prereqs.
assert(graph.targets.length == 1);
assert(graph.targets[0].prerequisites.length == 2,
// dep1.o + dep2.o stubs also materialized.
assert(graph.targets.length == 3);
auto myprog = graph.findTarget("my_prog");
assert(myprog !is null);
assert(myprog.prerequisites.length == 2,
"should have 2 real prereqs, got: " ~
graph.targets[0].prerequisites.length.to!string);
assert(graph.targets[0].prerequisites[0] == "dep1.o");
assert(graph.targets[0].prerequisites[1] == "dep2.o");
myprog.prerequisites.length.to!string);
assert(myprog.prerequisites[0] == "dep1.o");
assert(myprog.prerequisites[1] == "dep2.o");
// Scoped variable should be stored.
assert(env.getScoped("LDFLAGS", "my_prog") == "-lm",
@@ -0,0 +1 @@
# I'll use edit tool instead
+5
View File
@@ -770,6 +770,7 @@ bool isBuiltinFunction(string word)
case "wildcard": case "realpath": case "abspath":
case "shell": case "error": case "warning": case "info":
case "foreach": case "call": case "value": case "origin": case "flavor":
case "or": case "and":
return true;
default:
return false;
@@ -835,6 +836,8 @@ private bool isSelfExpanding(BuiltinFunction bf)
switch (bf)
{
case BuiltinFunction.foreach_:
case BuiltinFunction.or_:
case BuiltinFunction.and_:
return true;
default:
return false;
@@ -878,6 +881,8 @@ BuiltinFunction builtinFromName(string name)
case "value": return BuiltinFunction.value;
case "origin": return BuiltinFunction.origin;
case "flavor": return BuiltinFunction.flavor;
case "or": return BuiltinFunction.or_;
case "and": return BuiltinFunction.and_;
default: return BuiltinFunction.info;
}
}
+30
View File
@@ -568,6 +568,36 @@ string evaluateFunction(BuiltinFunction func, string[] args, Environment* env =
if (args.length < 1)
return "undefined";
return env && env.hasKey(args[0]) ? "recursive" : "undefined";
// --- Logical operators ---
case BuiltinFunction.or_:
// $(or arg1,arg2,...) — returns the first non-empty argument.
// Short-circuit: arguments are not expanded until needed.
foreach (arg; args)
{
// For self-expanding functions, args are raw — expand now.
import antelope.evaluator.expansion;
string expanded = expand(arg, env);
if (expanded.length > 0)
return expanded;
}
return "";
case BuiltinFunction.and_:
// $(and arg1,arg2,...) — returns empty if any argument is empty,
// otherwise returns the last (expanded) argument.
{
import antelope.evaluator.expansion;
string last;
foreach (arg; args)
{
string expanded = expand(arg, env);
if (expanded.length == 0)
return "";
last = expanded;
}
return last;
}
}
}
+2
View File
@@ -35,6 +35,8 @@ enum BuiltinFunction
value,
origin,
flavor,
or_,
and_,
}
/// A parsed function call.
+20 -1
View File
@@ -41,12 +41,20 @@ struct Token
}
/// Returns true if `c` is a valid character inside an identifier.
///
/// GNU Make identifiers are extremely permissive — almost any printable
/// character that isn't a whitespace or operator can appear in variable
/// names, values, and prerequisite names. Missing any character here
/// causes an infinite loop in the lexer (the identifier-accumulation
/// code breaks without advancing `pos`, so nextToken() returns the same
/// empty-token forever).
private static bool isIdentChar(char c)
{
import std.ascii : isAlphaNum;
switch (c)
{
case '-', '_', '.', '/', '+', '?', '%', '*', '~', '\\', '@', '<', '^':
case '-', '_', '.', '/', '+', '?', '%', '*', '~', '\\', '@', '<', '^',
'\'', '"', '`', '!', '&', '[', ']':
return true;
default:
return isAlphaNum(c);
@@ -274,6 +282,17 @@ struct Lexer
break;
}
// Safety net: if nothing was accumulated (the first character isn't
// a valid identifier char and we broke immediately), eat the
// unrecognised byte and return it as a single-char identifier.
// Without this, the lexer loops forever because `pos` never advances.
if (buf.length == 0 && pos < input.length)
{
buf ~= input[pos];
pos++;
column++;
}
string value = buf.idup;
return Token(TokenType.identifier, value, startLine, startCol);
}
+14 -1
View File
@@ -474,19 +474,32 @@ private struct Parser
prereq.data = accumulatePrereq();
// Merge adjacent tokens into a single prerequisite
// when the first token was a $ reference, but stop
// at standalone $ refs like $(OBJS) ${LIBS}
// at standalone $ refs like $(OBJS) ${LIBS}.
// Also stop if identifier text has been merged after
// the initial $() — a subsequent $ is a new independent
// reference (e.g., "$(srcdir)/foo $(BAR)" should be two
// prereqs, not one concatenation).
bool startsWithDollar = (prereq.data.length > 0 && prereq.data[0] == '$');
if (startsWithDollar)
{
bool mergedIdents;
while (check(TokenType.identifier) || check(TokenType.dollar))
{
// Don't merge separate standalone $ refs
if (check(TokenType.dollar) && (prereq.data[$-1] == ')' || prereq.data[$-1] == '}'))
break;
// Once any identifier has been merged after the $()
// reference (e.g., /POTFILES.in after $(srcdir)),
// subsequent tokens are new independent prereqs.
if (mergedIdents)
break;
if (check(TokenType.dollar))
prereq.data ~= accumulatePrereq();
else
{
prereq.data ~= advance().value;
mergedIdents = true;
}
}
}
rule.children ~= prereq;
+15 -1
View File
@@ -13,6 +13,7 @@ struct Environment
{
private string[string] vars;
private bool[string] exported;
private bool[string] cmdOverride; // set by command-line VAR=value
private ScopedVariable[] scopedVars;
/// Get a variable value.
@@ -29,9 +30,22 @@ struct Environment
return vars.keys;
}
/// Set a variable value.
/// Set a variable value (Makefile or environment).
/// Command-line overrides are NOT overwritten by this method.
void set(string key, string value)
{
// Command-line overrides have highest precedence —
// only `override` directive can change them (not yet implemented).
if (key in cmdOverride)
return;
vars[key] = value;
}
/// Set a command-line variable override (VAR=value on CLI).
/// These take precedence over all Makefile assignments.
void setCmdOverride(string key, string value)
{
cmdOverride[key] = true;
vars[key] = value;
}