diff --git a/1.0_ROADMAP b/1.0_ROADMAP new file mode 100644 index 0000000..bb02c5b --- /dev/null +++ b/1.0_ROADMAP @@ -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 diff --git a/source/antelope/build/target.d b/source/antelope/build/target.d index a6bcf07..669bd54 100644 --- a/source/antelope/build/target.d +++ b/source/antelope/build/target.d @@ -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 } diff --git a/source/antelope/cli/args.d b/source/antelope/cli/args.d index d2dc02c..8c5b94b 100644 --- a/source/antelope/cli/args.d +++ b/source/antelope/cli/args.d @@ -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; } diff --git a/source/antelope/cli/subcommands.d b/source/antelope/cli/subcommands.d index 8165973..cdd0d2f 100644 --- a/source/antelope/cli/subcommands.d +++ b/source/antelope/cli/subcommands.d @@ -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(); @@ -229,9 +258,26 @@ int runBuild(CliConfig config) { if (!graph.hasTarget(targetName)) { - log(LogLevel.normal, "antelope: *** No rule to make target '" ~ - targetName ~ "'. Stop."); - return 1; + // 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 @@ -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; diff --git a/source/antelope/compatibility/submake.d b/source/antelope/compatibility/submake.d index b2dd673..c92c887 100644 --- a/source/antelope/compatibility/submake.d +++ b/source/antelope/compatibility/submake.d @@ -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) diff --git a/source/antelope/evaluator/evaluator.d b/source/antelope/evaluator/evaluator.d index e755cc7..e21ea6a 100644 --- a/source/antelope/evaluator/evaluator.d +++ b/source/antelope/evaluator/evaluator.d @@ -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,9 +172,100 @@ size_t resolveImplicitRules(ref DependencyGraph graph, Environment* env = null) if (userMatches.length > 0) { auto um = userMatches[$ - 1]; - resolveUserPatternRule(tp, um, graph, env); - resolved++; - applied = true; + // 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; + } + } + } + } + } } } @@ -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", diff --git a/source/antelope/evaluator/evaluator.d.tmp b/source/antelope/evaluator/evaluator.d.tmp new file mode 100644 index 0000000..20d8c7e --- /dev/null +++ b/source/antelope/evaluator/evaluator.d.tmp @@ -0,0 +1 @@ +# I'll use edit tool instead diff --git a/source/antelope/evaluator/expansion.d b/source/antelope/evaluator/expansion.d index 3bbf533..f1bc284 100644 --- a/source/antelope/evaluator/expansion.d +++ b/source/antelope/evaluator/expansion.d @@ -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; } } diff --git a/source/antelope/evaluator/functions.d b/source/antelope/evaluator/functions.d index 06dcf7a..46e784d 100644 --- a/source/antelope/evaluator/functions.d +++ b/source/antelope/evaluator/functions.d @@ -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; + } } } diff --git a/source/antelope/parser/functions.d b/source/antelope/parser/functions.d index ca32ea3..06eac83 100644 --- a/source/antelope/parser/functions.d +++ b/source/antelope/parser/functions.d @@ -35,6 +35,8 @@ enum BuiltinFunction value, origin, flavor, + or_, + and_, } /// A parsed function call. diff --git a/source/antelope/parser/lexer.d b/source/antelope/parser/lexer.d index d1b1f05..ab2b524 100644 --- a/source/antelope/parser/lexer.d +++ b/source/antelope/parser/lexer.d @@ -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); } diff --git a/source/antelope/parser/parser.d b/source/antelope/parser/parser.d index e2944e6..9769d18 100644 --- a/source/antelope/parser/parser.d +++ b/source/antelope/parser/parser.d @@ -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; diff --git a/source/antelope/shell/environment.d b/source/antelope/shell/environment.d index b7cc11f..332a35f 100644 --- a/source/antelope/shell/environment.d +++ b/source/antelope/shell/environment.d @@ -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; }