Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac1fb89f9a | ||
|
|
188987d54a | ||
|
|
fb7fdf330f |
+12
@@ -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
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Antefile — builds Antelope itself.
|
||||||
|
|
||||||
|
OUT := antelope
|
||||||
|
|
||||||
|
all: debug
|
||||||
|
@echo "→ antelope built (debug)"
|
||||||
|
|
||||||
|
debug:
|
||||||
|
dub build
|
||||||
|
|
||||||
|
release:
|
||||||
|
dub build --compiler=ldc2
|
||||||
|
strip $(OUT)
|
||||||
|
@echo "→ antelope built (release, ldc2, stripped)"
|
||||||
|
|
||||||
|
dist: release
|
||||||
|
@tag=$$(git describe --tags --always 2>/dev/null || echo "dev"); \
|
||||||
|
date=$$(date +%Y%m%d); \
|
||||||
|
commit=$$(git rev-parse --short HEAD); \
|
||||||
|
name="antelope-$${tag}-$${date}-$${commit}"; \
|
||||||
|
mkdir -p "$$name"; \
|
||||||
|
cp $(OUT) "$$name/"; \
|
||||||
|
cp README.md LICENSE antefile "$$name/"; \
|
||||||
|
tar -cf - "$$name" | zstd -T0 -o "$$name.tar.zst"; \
|
||||||
|
rm -rf "$$name"; \
|
||||||
|
echo "→ $$name.tar.zst"
|
||||||
|
|
||||||
|
test:
|
||||||
|
dub test
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(OUT) *.o
|
||||||
|
|
||||||
|
dist-clean: clean
|
||||||
|
rm -f antelope-*.tar.zst
|
||||||
@@ -17,4 +17,5 @@ struct Target
|
|||||||
string[] prerequisites; /// Normal prerequisites (trigger rebuild)
|
string[] prerequisites; /// Normal prerequisites (trigger rebuild)
|
||||||
string[] recipe; /// Shell commands to build this target
|
string[] recipe; /// Shell commands to build this target
|
||||||
string[] orderOnlyPrereqs; /// Order-only prerequisites (| — must exist, no rebuild trigger)
|
string[] orderOnlyPrereqs; /// Order-only prerequisites (| — must exist, no rebuild trigger)
|
||||||
|
string stem; /// Pattern/suffix rule stem for $* expansion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module antelope.cli.args;
|
|||||||
|
|
||||||
import std.algorithm.searching : startsWith;
|
import std.algorithm.searching : startsWith;
|
||||||
import std.conv : to;
|
import std.conv : to;
|
||||||
import std.string : strip;
|
import std.string : strip, indexOf;
|
||||||
import std.ascii : isDigit;
|
import std.ascii : isDigit;
|
||||||
|
|
||||||
/// Available subcommands.
|
/// Available subcommands.
|
||||||
@@ -54,6 +54,10 @@ struct CliConfig
|
|||||||
|
|
||||||
/// Show version (--version).
|
/// Show version (--version).
|
||||||
bool showVersion;
|
bool showVersion;
|
||||||
|
|
||||||
|
/// Variable overrides from the command line (VAR=value style).
|
||||||
|
/// GNU Make passes these with highest precedence.
|
||||||
|
string[string] varOverrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
@@ -229,6 +233,16 @@ CliConfig parseArgs(string[] args)
|
|||||||
continue;
|
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
|
// Everything else is a target
|
||||||
config.targets ~= arg;
|
config.targets ~= arg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,35 @@ int runBuild(CliConfig config)
|
|||||||
env.set("MAKECMDGOALS", config.targets.join(" "));
|
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
|
// Evaluate AST → populate env + build graph
|
||||||
auto graph = new DependencyGraph();
|
auto graph = new DependencyGraph();
|
||||||
|
|
||||||
@@ -229,9 +258,26 @@ int runBuild(CliConfig config)
|
|||||||
{
|
{
|
||||||
if (!graph.hasTarget(targetName))
|
if (!graph.hasTarget(targetName))
|
||||||
{
|
{
|
||||||
log(LogLevel.normal, "antelope: *** No rule to make target '" ~
|
// Target not explicitly defined — try to create it from
|
||||||
targetName ~ "'. Stop.");
|
// implicit rules (suffix rules, pattern rules). Autotools
|
||||||
return 1;
|
// 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
|
// Resolve dependencies and check what needs building
|
||||||
@@ -263,7 +309,7 @@ int runBuild(CliConfig config)
|
|||||||
{
|
{
|
||||||
// Expand variables in the recipe
|
// Expand variables in the recipe
|
||||||
string expanded = expand(recipeLine, env, t.name,
|
string expanded = expand(recipeLine, env, t.name,
|
||||||
t.prerequisites);
|
t.prerequisites, t.stem);
|
||||||
|
|
||||||
// Print the command unless silent (@ prefix)
|
// Print the command unless silent (@ prefix)
|
||||||
import std.string : stripLeft;
|
import std.string : stripLeft;
|
||||||
|
|||||||
@@ -40,11 +40,17 @@ struct SubMakeConfig
|
|||||||
/// -n — dry run
|
/// -n — dry run
|
||||||
/// -P — POSIX conformance mode
|
/// -P — POSIX conformance mode
|
||||||
/// -d — debug output
|
/// -d — debug output
|
||||||
|
/// VAR=val — command-line variable overrides
|
||||||
///
|
///
|
||||||
/// Returns: a space-delimited MAKEFLAGS string, or "" if no flags are active.
|
/// Returns: a space-delimited MAKEFLAGS string, or "" if no flags are active.
|
||||||
string serializeMakeFlags(CliConfig config)
|
string serializeMakeFlags(CliConfig config)
|
||||||
{
|
{
|
||||||
string flags;
|
string flags;
|
||||||
|
|
||||||
|
// Variable overrides first (parsed back by parseArgs in sub-make)
|
||||||
|
foreach (varName, varValue; config.varOverrides)
|
||||||
|
flags ~= " " ~ varName ~ "=" ~ varValue;
|
||||||
|
|
||||||
if (config.jobs > 1)
|
if (config.jobs > 1)
|
||||||
flags ~= " -j" ~ config.jobs.to!string;
|
flags ~= " -j" ~ config.jobs.to!string;
|
||||||
if (config.dryRun)
|
if (config.dryRun)
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ size_t resolveImplicitRules(ref DependencyGraph graph, Environment* env = null)
|
|||||||
if (prereqSatisfiable(match.resolvedPrereq, graph))
|
if (prereqSatisfiable(match.resolvedPrereq, graph))
|
||||||
{
|
{
|
||||||
tp.recipe = match.rule.recipe;
|
tp.recipe = match.rule.recipe;
|
||||||
|
tp.stem = match.stem; // for $* expansion in recipes
|
||||||
if (match.resolvedPrereq.length > 0)
|
if (match.resolvedPrereq.length > 0)
|
||||||
{
|
{
|
||||||
bool alreadyPresent;
|
bool alreadyPresent;
|
||||||
@@ -171,9 +172,100 @@ size_t resolveImplicitRules(ref DependencyGraph graph, Environment* env = null)
|
|||||||
if (userMatches.length > 0)
|
if (userMatches.length > 0)
|
||||||
{
|
{
|
||||||
auto um = userMatches[$ - 1];
|
auto um = userMatches[$ - 1];
|
||||||
resolveUserPatternRule(tp, um, graph, env);
|
// Skip pattern rules with no recipe — GNU Make treats these
|
||||||
resolved++;
|
// as "cancellation" rules that remove implicit rules rather
|
||||||
applied = true;
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +285,7 @@ private void resolveUserPatternRule(Target* tp, PatternMatch match,
|
|||||||
ref DependencyGraph graph, Environment* env)
|
ref DependencyGraph graph, Environment* env)
|
||||||
{
|
{
|
||||||
tp.recipe = match.rule.recipe;
|
tp.recipe = match.rule.recipe;
|
||||||
|
tp.stem = match.stem; // for $* expansion in recipes
|
||||||
// Clear existing prereqs and set from pattern match
|
// Clear existing prereqs and set from pattern match
|
||||||
tp.prerequisites = [];
|
tp.prerequisites = [];
|
||||||
foreach (prereq; match.resolvedPrereqs)
|
foreach (prereq; match.resolvedPrereqs)
|
||||||
@@ -337,7 +430,11 @@ private void handleRule(AstNode node, Environment* env, DependencyGraph* graph)
|
|||||||
foreach (ref p; prereqs)
|
foreach (ref p; prereqs)
|
||||||
{
|
{
|
||||||
string expanded = expand(p, env);
|
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)
|
if (word.length > 0)
|
||||||
expandedPrereqs ~= word;
|
expandedPrereqs ~= word;
|
||||||
@@ -387,12 +484,35 @@ private void handleRule(AstNode node, Environment* env, DependencyGraph* graph)
|
|||||||
t.orderOnlyPrereqs = prereqSplit.orderOnly;
|
t.orderOnlyPrereqs = prereqSplit.orderOnly;
|
||||||
t.recipe = recipe;
|
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,
|
// Handle multi-target rules: when target expands to multiple words,
|
||||||
// create separate targets for each (same prereqs + recipe).
|
// create separate targets for each (same prereqs + recipe).
|
||||||
import std.string;
|
import std.string;
|
||||||
import std.algorithm : filter;
|
import std.algorithm : filter;
|
||||||
import std.array : array;
|
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)
|
if (expandedTargets.length > 1)
|
||||||
{
|
{
|
||||||
foreach (tn; expandedTargets)
|
foreach (tn; expandedTargets)
|
||||||
@@ -561,7 +681,7 @@ private void handleDirective(AstNode node, Environment* env, DependencyGraph* gr
|
|||||||
import std.string : split;
|
import std.string : split;
|
||||||
VPathEntry entry;
|
VPathEntry entry;
|
||||||
entry.pattern = pattern;
|
entry.pattern = pattern;
|
||||||
foreach (dir; dirs.split(" "))
|
foreach (dir; dirs.split())
|
||||||
if (dir.length > 0) entry.directories ~= dir;
|
if (dir.length > 0) entry.directories ~= dir;
|
||||||
// Store in env for later use by the resolver
|
// Store in env for later use by the resolver
|
||||||
if (env !is null)
|
if (env !is null)
|
||||||
@@ -667,14 +787,21 @@ private void handleInclude(string dirName, string data, ptrdiff_t space,
|
|||||||
{
|
{
|
||||||
if (space < 0) return;
|
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;
|
if (rawPath.length == 0) return;
|
||||||
|
|
||||||
// Expand variable references in the include path (e.g., $(DEP_FILES)
|
// Expand variable references in the include path (e.g., $(DEP_FILES)
|
||||||
// commonly expands to "./.deps/a.Po ./.deps/b.Po" in autotools projects).
|
// commonly expands to "./.deps/a.Po ./.deps/b.Po" in autotools projects).
|
||||||
import antelope.evaluator.expansion;
|
import antelope.evaluator.expansion;
|
||||||
import std.string : split;
|
import std.string : split;
|
||||||
auto paths = expand(rawPath, env).split(" ");
|
auto paths = expand(rawPath, env).split();
|
||||||
|
|
||||||
foreach (path; paths)
|
foreach (path; paths)
|
||||||
{
|
{
|
||||||
@@ -812,20 +939,23 @@ unittest
|
|||||||
|
|
||||||
evaluate(root, &env, &graph);
|
evaluate(root, &env, &graph);
|
||||||
|
|
||||||
assert(graph.targets.length == 1,
|
// With prerequisite materialization, main.o + util.o become stub targets
|
||||||
"graph should contain exactly 1 target");
|
assert(graph.targets.length == 3,
|
||||||
assert(graph.targets[0].name == "program",
|
"graph should contain program + 2 prereq stubs");
|
||||||
"target name should be 'program', got: " ~ graph.targets[0].name);
|
auto prog = graph.findTarget("program");
|
||||||
assert(graph.targets[0].prerequisites.length == 2,
|
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: " ~
|
"target should have 2 prerequisites, got: " ~
|
||||||
to!string(graph.targets[0].prerequisites.length));
|
to!string(prog.prerequisites.length));
|
||||||
assert(graph.targets[0].prerequisites[0] == "main.o",
|
assert(prog.prerequisites[0] == "main.o",
|
||||||
"first prereq should be 'main.o', got: " ~
|
"first prereq should be 'main.o', got: " ~
|
||||||
graph.targets[0].prerequisites[0]);
|
prog.prerequisites[0]);
|
||||||
assert(graph.targets[0].prerequisites[1] == "util.o",
|
assert(prog.prerequisites[1] == "util.o",
|
||||||
"second prereq should be 'util.o', got: " ~
|
"second prereq should be 'util.o', got: " ~
|
||||||
graph.targets[0].prerequisites[1]);
|
prog.prerequisites[1]);
|
||||||
assert(graph.targets[0].recipe.length == 1,
|
assert(prog.recipe.length == 1,
|
||||||
"target should have 1 recipe line");
|
"target should have 1 recipe line");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,9 +1260,11 @@ unittest
|
|||||||
|
|
||||||
assert(env.get("CC") == "gcc");
|
assert(env.get("CC") == "gcc");
|
||||||
assert(env.get("CFLAGS") == "-Wall");
|
assert(env.get("CFLAGS") == "-Wall");
|
||||||
assert(graph.targets.length == 2);
|
// foo.o: foo.c → foo.c stub, bar.o: → no prereqs
|
||||||
assert(graph.targets[0].name == "foo.o");
|
assert(graph.targets.length == 3);
|
||||||
assert(graph.targets[1].name == "bar.o");
|
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);
|
evaluate(root, &env, &graph);
|
||||||
|
|
||||||
// Target should exist with only real prereqs.
|
// Target should exist with only real prereqs.
|
||||||
// Target should exist with only real prereqs.
|
// dep1.o + dep2.o stubs also materialized.
|
||||||
assert(graph.targets.length == 1);
|
assert(graph.targets.length == 3);
|
||||||
assert(graph.targets[0].prerequisites.length == 2,
|
auto myprog = graph.findTarget("my_prog");
|
||||||
|
assert(myprog !is null);
|
||||||
|
assert(myprog.prerequisites.length == 2,
|
||||||
"should have 2 real prereqs, got: " ~
|
"should have 2 real prereqs, got: " ~
|
||||||
graph.targets[0].prerequisites.length.to!string);
|
myprog.prerequisites.length.to!string);
|
||||||
assert(graph.targets[0].prerequisites[0] == "dep1.o");
|
assert(myprog.prerequisites[0] == "dep1.o");
|
||||||
assert(graph.targets[0].prerequisites[1] == "dep2.o");
|
assert(myprog.prerequisites[1] == "dep2.o");
|
||||||
|
|
||||||
// Scoped variable should be stored.
|
// Scoped variable should be stored.
|
||||||
assert(env.getScoped("LDFLAGS", "my_prog") == "-lm",
|
assert(env.getScoped("LDFLAGS", "my_prog") == "-lm",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# I'll use edit tool instead
|
||||||
@@ -224,11 +224,13 @@ private string resolveParenContent(string content, char closer, Environment* env
|
|||||||
content[colonIdx + 1] != '=' && indexOf(content[colonIdx + 1 .. $], '=') >= 0)
|
content[colonIdx + 1] != '=' && indexOf(content[colonIdx + 1 .. $], '=') >= 0)
|
||||||
{
|
{
|
||||||
// Syntax: VAR:old=new
|
// Syntax: VAR:old=new
|
||||||
|
// Per GNU Make: $(var:a=b) ≡ $(patsubst %a,%b,$(var))
|
||||||
|
// The '%' is prepended so that patsubst matches at end-of-word.
|
||||||
string varName = content[0 .. colonIdx];
|
string varName = content[0 .. colonIdx];
|
||||||
auto eqIdx = indexOf(content[colonIdx + 1 .. $], '=');
|
auto eqIdx = indexOf(content[colonIdx + 1 .. $], '=');
|
||||||
string oldPat = content[colonIdx + 1 .. colonIdx + 1 + eqIdx];
|
string oldPat = "%" ~ content[colonIdx + 1 .. colonIdx + 1 + eqIdx];
|
||||||
string newPat = content[colonIdx + 1 + eqIdx + 1 .. $];
|
string newPat = "%" ~ content[colonIdx + 1 + eqIdx + 1 .. $];
|
||||||
// Equivalent to $(patsubst oldPat,newPat,$(varName))
|
// Equivalent to $(patsubst %oldPat,%newPat,$(varName))
|
||||||
string varValue = env ? env.get(varName) : "";
|
string varValue = env ? env.get(varName) : "";
|
||||||
if (varValue.length > 0)
|
if (varValue.length > 0)
|
||||||
{
|
{
|
||||||
@@ -768,6 +770,7 @@ bool isBuiltinFunction(string word)
|
|||||||
case "wildcard": case "realpath": case "abspath":
|
case "wildcard": case "realpath": case "abspath":
|
||||||
case "shell": case "error": case "warning": case "info":
|
case "shell": case "error": case "warning": case "info":
|
||||||
case "foreach": case "call": case "value": case "origin": case "flavor":
|
case "foreach": case "call": case "value": case "origin": case "flavor":
|
||||||
|
case "or": case "and":
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -833,6 +836,8 @@ private bool isSelfExpanding(BuiltinFunction bf)
|
|||||||
switch (bf)
|
switch (bf)
|
||||||
{
|
{
|
||||||
case BuiltinFunction.foreach_:
|
case BuiltinFunction.foreach_:
|
||||||
|
case BuiltinFunction.or_:
|
||||||
|
case BuiltinFunction.and_:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -876,6 +881,8 @@ BuiltinFunction builtinFromName(string name)
|
|||||||
case "value": return BuiltinFunction.value;
|
case "value": return BuiltinFunction.value;
|
||||||
case "origin": return BuiltinFunction.origin;
|
case "origin": return BuiltinFunction.origin;
|
||||||
case "flavor": return BuiltinFunction.flavor;
|
case "flavor": return BuiltinFunction.flavor;
|
||||||
|
case "or": return BuiltinFunction.or_;
|
||||||
|
case "and": return BuiltinFunction.and_;
|
||||||
default: return BuiltinFunction.info;
|
default: return BuiltinFunction.info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -568,6 +568,36 @@ string evaluateFunction(BuiltinFunction func, string[] args, Environment* env =
|
|||||||
if (args.length < 1)
|
if (args.length < 1)
|
||||||
return "undefined";
|
return "undefined";
|
||||||
return env && env.hasKey(args[0]) ? "recursive" : "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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ enum BuiltinFunction
|
|||||||
value,
|
value,
|
||||||
origin,
|
origin,
|
||||||
flavor,
|
flavor,
|
||||||
|
or_,
|
||||||
|
and_,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A parsed function call.
|
/// A parsed function call.
|
||||||
|
|||||||
@@ -41,12 +41,20 @@ struct Token
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if `c` is a valid character inside an identifier.
|
/// 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)
|
private static bool isIdentChar(char c)
|
||||||
{
|
{
|
||||||
import std.ascii : isAlphaNum;
|
import std.ascii : isAlphaNum;
|
||||||
switch (c)
|
switch (c)
|
||||||
{
|
{
|
||||||
case '-', '_', '.', '/', '+', '?', '%', '*', '~', '\\', '@', '<', '^':
|
case '-', '_', '.', '/', '+', '?', '%', '*', '~', '\\', '@', '<', '^',
|
||||||
|
'\'', '"', '`', '!', '&', '[', ']':
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return isAlphaNum(c);
|
return isAlphaNum(c);
|
||||||
@@ -274,6 +282,17 @@ struct Lexer
|
|||||||
break;
|
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;
|
string value = buf.idup;
|
||||||
return Token(TokenType.identifier, value, startLine, startCol);
|
return Token(TokenType.identifier, value, startLine, startCol);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,19 +474,32 @@ private struct Parser
|
|||||||
prereq.data = accumulatePrereq();
|
prereq.data = accumulatePrereq();
|
||||||
// Merge adjacent tokens into a single prerequisite
|
// Merge adjacent tokens into a single prerequisite
|
||||||
// when the first token was a $ reference, but stop
|
// 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] == '$');
|
bool startsWithDollar = (prereq.data.length > 0 && prereq.data[0] == '$');
|
||||||
if (startsWithDollar)
|
if (startsWithDollar)
|
||||||
{
|
{
|
||||||
|
bool mergedIdents;
|
||||||
while (check(TokenType.identifier) || check(TokenType.dollar))
|
while (check(TokenType.identifier) || check(TokenType.dollar))
|
||||||
{
|
{
|
||||||
// Don't merge separate standalone $ refs
|
// Don't merge separate standalone $ refs
|
||||||
if (check(TokenType.dollar) && (prereq.data[$-1] == ')' || prereq.data[$-1] == '}'))
|
if (check(TokenType.dollar) && (prereq.data[$-1] == ')' || prereq.data[$-1] == '}'))
|
||||||
break;
|
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))
|
if (check(TokenType.dollar))
|
||||||
prereq.data ~= accumulatePrereq();
|
prereq.data ~= accumulatePrereq();
|
||||||
else
|
else
|
||||||
|
{
|
||||||
prereq.data ~= advance().value;
|
prereq.data ~= advance().value;
|
||||||
|
mergedIdents = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rule.children ~= prereq;
|
rule.children ~= prereq;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct Environment
|
|||||||
{
|
{
|
||||||
private string[string] vars;
|
private string[string] vars;
|
||||||
private bool[string] exported;
|
private bool[string] exported;
|
||||||
|
private bool[string] cmdOverride; // set by command-line VAR=value
|
||||||
private ScopedVariable[] scopedVars;
|
private ScopedVariable[] scopedVars;
|
||||||
|
|
||||||
/// Get a variable value.
|
/// Get a variable value.
|
||||||
@@ -29,9 +30,22 @@ struct Environment
|
|||||||
return vars.keys;
|
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)
|
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;
|
vars[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user