commit 07d575be5f820ea9870be8a6cdb25e760fc13df5
parent e88492d2c78696639fb98f866cf81cc77764f97e
Author: Autumn! <autumnull@posteo.net>
Date: Fri, 3 Mar 2023 13:16:54 +0000
Implement deps subcommand
Signed-off-by: Autumn! <autumnull@posteo.net>
Diffstat:
M | Makefile | | | 1 | + |
A | cmd/hare/deps.ha | | | 122 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | cmd/hare/subcmds.ha | | | 111 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
3 files changed, 231 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
@@ -23,6 +23,7 @@ all:
include stdlib.mk
hare_srcs = \
+ ./cmd/hare/deps.ha \
./cmd/hare/main.ha \
./cmd/hare/plan.ha \
./cmd/hare/progress.ha \
diff --git a/cmd/hare/deps.ha b/cmd/hare/deps.ha
@@ -0,0 +1,122 @@
+use fmt;
+use hare::module;
+use hare::parse;
+use io;
+use os;
+use sort;
+use strings;
+
+type depnode = struct {
+ ident: str,
+ depends: []size,
+ depth: uint,
+};
+
+// the start of the cycle in the stack
+type dep_cycle = !size;
+
+// depth-first initial exploration, cycle-detection, reverse topological sort
+fn explore_deps(ctx: *module::context, stack: *[]str, visited: *[]depnode, ident: str) (size | dep_cycle) = {
+ // check for cycles
+ for (let i = 0z; i < len(stack); i += 1) {
+ if (ident == stack[i]) {
+ append(stack, ident);
+ return i: dep_cycle;
+ };
+ };
+
+ // return existing depnode if visited already
+ for (let i = 0z; i < len(visited); i += 1) {
+ if (ident == visited[i].ident) return i;
+ };
+ append(stack, ident);
+
+ let this = depnode{ident = strings::dup(ident), depends = [], depth = 0};
+ let ver = match (module::lookup(ctx, parse::identstr(ident)!)) {
+ case let e: module::error =>
+ fmt::fatal(module::strerror(e));
+ case let ver: module::version =>
+ yield ver;
+ };
+ for (let i = 0z; i < len(ver.depends); i += 1) {
+ const name = strings::join("::", ver.depends[i]...);
+ defer free(name);
+ const child = explore_deps(ctx, stack, visited, name)?;
+ append(this.depends, child);
+ };
+ // reverse-sort depends so that we know the last in the list is the
+ // "final" child during show_deps
+ sort::sort(this.depends, size(size), &cmpsz);
+
+ static delete(stack[len(stack)-1]);
+ append(visited, this);
+ return len(visited) - 1;
+};
+
+// sorts in reverse
+fn cmpsz(a: const *void, b: const *void) int = (*(b: *size) - *(a: *size)): int;
+
+type link = struct {
+ depth: uint,
+ child: size,
+ final: bool,
+};
+
+fn show_deps(depnodes: *[]depnode) void = {
+ let links: []link = [];
+ defer free(links);
+ // traverse in reverse because reverse-topo-sort
+ for (let i = len(depnodes) - 1; 0 <= i && i < len(depnodes); i -= 1) {
+ for (let j = 0z; j < len(links); j += 1) {
+ if (i < links[j].child) continue;
+ if (depnodes[i].depth < links[j].depth + 1) depnodes[i].depth = links[j].depth + 1;
+ };
+
+ // print in-between row
+ for (let d = 0u; d < depnodes[i].depth; d += 1) {
+ let passing = false;
+ for (let j = 0z; j < len(links); j += 1) {
+ if (i < links[j].child) continue;
+ if (d == links[j].depth) {
+ passing = true;
+ };
+ };
+ fmt::print(if (passing) "│ " else " ")!;
+ };
+ if (i < len(depnodes) - 1) fmt::println()!;
+
+ // print row itself
+ let on_path = false;
+ for (let d = 0u; d < depnodes[i].depth; d += 1) {
+ let connected = false;
+ let passing = false;
+ let final = false;
+ for (let j = 0z; j < len(links); j += 1) {
+ if (i < links[j].child) continue;
+ if (d == links[j].depth) {
+ passing = true;
+ if (i == links[j].child) {
+ connected = true;
+ on_path = true;
+ if (links[j].final) final = true;
+ };
+ };
+ };
+ fmt::print(
+ if (final) "└──"
+ else if (connected) "├──"
+ else if (on_path) "───"
+ else if (passing) "│ "
+ else " "
+ )!;
+ };
+ fmt::println(depnodes[i].ident)!;
+ for (let j = 0z; j < len(depnodes[i].depends); j += 1) {
+ append(links, link{
+ depth = depnodes[i].depth,
+ child = depnodes[i].depends[j],
+ final = len(depnodes[i].depends) == j + 1,
+ });
+ };
+ };
+};
diff --git a/cmd/hare/subcmds.ha b/cmd/hare/subcmds.ha
@@ -16,6 +16,7 @@ use io;
use os::exec;
use os;
use path;
+use sort;
use strings;
use unix::tty;
@@ -209,19 +210,123 @@ fn cache(args: []str) void = {
abort("cache subcommand not implemented yet."); // TODO
};
+type deps_goal = enum {
+ DOT,
+ MAKE,
+ TERM,
+};
+
fn deps(args: []str) void = {
const help: []getopt::help = [
"prints dependency information for a Hare program",
('d', "print dot syntax for use with graphviz"),
- ('M', "print rules for POSIX make"),
+ ('M', "build-dir", "print rules for POSIX make"),
('T', "tags...", "set build tags"),
('X', "tags...", "unset build tags"),
- "<path>",
+ "<path|module>",
];
const cmd = getopt::parse(args, help...);
defer getopt::finish(&cmd);
- abort("deps subcommand not implemented yet."); // TODO
+ let build_target = default_target();
+ let tags = module::tags_dup(build_target.tags);
+ defer module::tags_free(tags);
+
+ let build_dir: str = "";
+ let goal = deps_goal::TERM;
+ for (let i = 0z; i < len(cmd.opts); i += 1) {
+ let opt = cmd.opts[i];
+ switch (opt.0) {
+ case 'd' =>
+ goal = deps_goal::DOT;
+ case 'M' =>
+ goal = deps_goal::MAKE;
+ build_dir = opt.1;
+ case 'T' =>
+ tags = match (addtags(tags, opt.1)) {
+ case void =>
+ fmt::fatal("Error parsing tags");
+ case let t: []module::tag =>
+ yield t;
+ };
+ case 'X' =>
+ tags = match (deltags(tags, opt.1)) {
+ case void =>
+ fmt::fatal("Error parsing tags");
+ case let t: []module::tag =>
+ yield t;
+ };
+ case =>
+ abort();
+ };
+ };
+
+ const input =
+ if (len(cmd.args) == 0) os::getcwd()
+ else if (len(cmd.args) == 1) cmd.args[0]
+ else {
+ getopt::printusage(os::stderr, args[0], help...);
+ os::exit(1);
+ };
+
+ const ctx = module::context_init(tags, [], HAREPATH);
+ defer module::context_finish(&ctx);
+
+ const ver = match (parse::identstr(input)) {
+ case let ident: ast::ident =>
+ yield match (module::lookup(&ctx, ident)) {
+ case let ver: module::version =>
+ yield ver;
+ case let err: module::error =>
+ fmt::fatal("Error scanning input module:",
+ module::strerror(err));
+ };
+ case parse::error =>
+ yield match (module::scan(&ctx, input)) {
+ case let ver: module::version =>
+ yield ver;
+ case let err: module::error =>
+ fmt::fatal("Error scanning input path:",
+ module::strerror(err));
+ };
+ };
+
+ let visited: []depnode = [];
+ let stack: []str = [];
+ defer free(stack);
+ const ctx = module::context_init([], [], HAREPATH);
+
+ let toplevel = depnode{ident = strings::dup(path::basename(input)), depends = [], depth = 0};
+
+ for (let i = 0z; i < len(ver.depends); i += 1) {
+ const name = strings::join("::", ver.depends[i]...);
+ defer free(name);
+ const child = match (explore_deps(&ctx, &stack, &visited, name)) {
+ case let index: size => yield index;
+ case let start: dep_cycle =>
+ const chain = strings::join(" -> ", stack[start..]...);
+ defer free(chain);
+ fmt::errorln("Dependency cycle detected:", chain)!;
+ os::exit(1);
+ };
+ append(toplevel.depends, child);
+ };
+
+ sort::sort(toplevel.depends, size(size), &cmpsz);
+ append(visited, toplevel);
+ defer for (let i = 0z; i < len(visited); i += 1) {
+ free(visited[i].ident);
+ free(visited[i].depends);
+ };
+
+ switch (goal) {
+ case deps_goal::TERM =>
+ show_deps(&visited);
+ case deps_goal::DOT =>
+ abort("-d option not implemented yet");
+ case deps_goal::MAKE =>
+ abort("-M option not implemented yet");
+ };
};
fn release(args: []str) void = {