hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

subcmds.ha (14831B)


      1 // License: GPL-3.0
      2 // (c) 2021-2022 Alexey Yerin <yyp@disroot.org>
      3 // (c) 2021 Drew DeVault <sir@cmpwn.com>
      4 // (c) 2021 Ember Sawady <ecs@d2evs.net>
      5 use ascii;
      6 use bufio;
      7 use encoding::utf8;
      8 use errors;
      9 use fmt;
     10 use fs;
     11 use getopt;
     12 use hare::ast;
     13 use hare::module;
     14 use hare::parse;
     15 use io;
     16 use os::exec;
     17 use os;
     18 use path;
     19 use sort;
     20 use strings;
     21 use unix::tty;
     22 
     23 fn addtags(tags: []module::tag, in: str) ([]module::tag | void) = {
     24 	let in = match (module::parsetags(in)) {
     25 	case void =>
     26 		return void;
     27 	case let t: []module::tag =>
     28 		yield t;
     29 	};
     30 	defer free(in);
     31 	append(tags, in...);
     32 	return tags;
     33 };
     34 
     35 fn deltags(tags: []module::tag, in: str) ([]module::tag | void) = {
     36 	if (in == "^") {
     37 		module::tags_free(tags);
     38 		return [];
     39 	};
     40 	let in = match (module::parsetags(in)) {
     41 	case void =>
     42 		return void;
     43 	case let t: []module::tag =>
     44 		yield t;
     45 	};
     46 	defer free(in);
     47 	for (let i = 0z; i < len(tags); i += 1) {
     48 		for (let j = 0z; j < len(in); j += 1) {
     49 			if (tags[i].name == in[j].name
     50 					&& tags[i].mode == in[j].mode) {
     51 				free(tags[i].name);
     52 				delete(tags[i]);
     53 				i -= 1;
     54 			};
     55 		};
     56 	};
     57 	return tags;
     58 };
     59 
     60 type goal = enum {
     61 	OBJ,
     62 	EXE,
     63 };
     64 
     65 fn build(cmd: *getopt::command) void = {
     66 	let build_target = default_target();
     67 	let tags = module::tags_dup(build_target.tags);
     68 	defer module::tags_free(tags);
     69 
     70 	let verbose = false;
     71 	let output = "";
     72 	let goal = goal::EXE;
     73 	let defines: []str = [];
     74 	defer free(defines);
     75 	let libdir: []str = [];
     76 	defer free(libdir);
     77 	let libs: []str = [];
     78 	defer free(libs);
     79 	let namespace: ast::ident = [];
     80 	for (let i = 0z; i < len(cmd.opts); i += 1) {
     81 		let opt = cmd.opts[i];
     82 		switch (opt.0) {
     83 		case 'c' =>
     84 			goal = goal::OBJ;
     85 		case 'v' =>
     86 			verbose = true;
     87 		case 'D' =>
     88 			append(defines, opt.1);
     89 		case 'j' =>
     90 			abort("-j option not implemented yet."); // TODO
     91 		case 'L' =>
     92 			append(libdir, opt.1);
     93 		case 'l' =>
     94 			append(libs, opt.1);
     95 		case 'N' =>
     96 			namespace = match (parse::identstr(opt.1)) {
     97 			case let id: ast::ident =>
     98 				yield id;
     99 			case let err: parse::error =>
    100 				fmt::fatalf("Error parsing namespace {}: {}",
    101 					opt.1, parse::strerror(err));
    102 			};
    103 		case 'o' =>
    104 			output = opt.1;
    105 		case 't' =>
    106 			match (get_target(opt.1)) {
    107 			case void =>
    108 				fmt::fatalf("Unsupported target '{}'", opt.1);
    109 			case let t: *target =>
    110 				build_target = t;
    111 				module::tags_free(tags);
    112 				tags = module::tags_dup(t.tags);
    113 			};
    114 		case 'T' =>
    115 			tags = match (addtags(tags, opt.1)) {
    116 			case void =>
    117 				fmt::fatal("Error parsing tags");
    118 			case let t: []module::tag =>
    119 				yield t;
    120 			};
    121 		case 'X' =>
    122 			tags = match (deltags(tags, opt.1)) {
    123 			case void =>
    124 				fmt::fatal("Error parsing tags");
    125 			case let t: []module::tag =>
    126 				yield t;
    127 			};
    128 		case =>
    129 			abort();
    130 		};
    131 	};
    132 
    133 	const input =
    134 		if (len(cmd.args) == 0) os::getcwd()
    135 		else if (len(cmd.args) == 1) cmd.args[0]
    136 		else {
    137 			getopt::printusage(os::stderr, "build", cmd.help...)!;
    138 			os::exit(1);
    139 		};
    140 
    141 	if (len(libs) > 0) {
    142 		append(tags, module::tag {
    143 			mode = module::tag_mode::INCLUSIVE,
    144 			name = strings::dup("libc"),
    145 		});
    146 	};
    147 
    148 	const ctx = module::context_init(tags, defines, HAREPATH);
    149 	defer module::context_finish(&ctx);
    150 
    151 	const plan = mkplan(&ctx, libdir, libs, build_target);
    152 	defer plan_finish(&plan);
    153 
    154 	const ver = match (module::scan(&ctx, input)) {
    155 	case let ver: module::version =>
    156 		yield ver;
    157 	case let err: module::error =>
    158 		fmt::fatal("Error scanning input module:",
    159 			module::strerror(err));
    160 	};
    161 
    162 	const depends: []*task = [];
    163 	sched_module(&plan, ["rt"], &depends);
    164 
    165 	for (let i = 0z; i < len(ver.depends); i += 1z) {
    166 		const dep = ver.depends[i];
    167 		sched_module(&plan, dep, &depends);
    168 	};
    169 
    170 	// TODO: Choose this more intelligently
    171 	if (output == "") {
    172 		output = path::basename(ver.basedir);
    173 	};
    174 	switch (goal) {
    175 	case goal::EXE =>
    176 		sched_hare_exe(&plan, ver, output, depends...);
    177 	case goal::OBJ =>
    178 		let task = sched_hare_object(&plan, ver,
    179 			namespace, output, depends...);
    180 		append(plan.scheduled, task);
    181 	};
    182 	match (plan_execute(&plan, verbose)) {
    183 	case void => void;
    184 	case !exec::exit_status =>
    185 		fmt::fatalf("{} build: build failed", os::args[0]);
    186 	};
    187 };
    188 
    189 fn cache(cmd: *getopt::command) void = {
    190 	abort("cache subcommand not implemented yet."); // TODO
    191 };
    192 
    193 type deps_goal = enum {
    194 	DOT,
    195 	MAKE,
    196 	TERM,
    197 };
    198 
    199 fn deps(cmd: *getopt::command) void = {
    200 	let build_target = default_target();
    201 	let tags = module::tags_dup(build_target.tags);
    202 	defer module::tags_free(tags);
    203 
    204 	let build_dir: str = "";
    205 	let goal = deps_goal::TERM;
    206 	for (let i = 0z; i < len(cmd.opts); i += 1) {
    207 		let opt = cmd.opts[i];
    208 		switch (opt.0) {
    209 		case 'd' =>
    210 			goal = deps_goal::DOT;
    211 		case 'M' =>
    212 			goal = deps_goal::MAKE;
    213 			build_dir = opt.1;
    214 		case 'T' =>
    215 			tags = match (addtags(tags, opt.1)) {
    216 			case void =>
    217 				fmt::fatal("Error parsing tags");
    218 			case let t: []module::tag =>
    219 				yield t;
    220 			};
    221 		case 'X' =>
    222 			tags = match (deltags(tags, opt.1)) {
    223 			case void =>
    224 				fmt::fatal("Error parsing tags");
    225 			case let t: []module::tag =>
    226 				yield t;
    227 			};
    228 		case =>
    229 			abort();
    230 		};
    231 	};
    232 
    233 	const input =
    234 		if (len(cmd.args) == 0) os::getcwd()
    235 		else if (len(cmd.args) == 1) cmd.args[0]
    236 		else {
    237 			getopt::printusage(os::stderr, "deps", cmd.help...)!;
    238 			os::exit(1);
    239 		};
    240 
    241 	const ctx = module::context_init(tags, [], HAREPATH);
    242 	defer module::context_finish(&ctx);
    243 
    244 	const ver = match (parse::identstr(input)) {
    245 	case let ident: ast::ident =>
    246 		yield match (module::lookup(&ctx, ident)) {
    247 		case let ver: module::version =>
    248 			yield ver;
    249 		case let err: module::error =>
    250 			fmt::fatal("Error scanning input module:",
    251 				module::strerror(err));
    252 		};
    253 	case parse::error =>
    254 		yield match (module::scan(&ctx, input)) {
    255 		case let ver: module::version =>
    256 			yield ver;
    257 		case let err: module::error =>
    258 			fmt::fatal("Error scanning input path:",
    259 				module::strerror(err));
    260 		};
    261 	};
    262 
    263 	let visited: []depnode = [];
    264 	let stack: []str = [];
    265 	defer free(stack);
    266 	const ctx = module::context_init([], [], HAREPATH);
    267 
    268 	let toplevel = depnode{ident = strings::dup(path::basename(input)), depends = [], depth = 0};
    269 
    270 	for (let i = 0z; i < len(ver.depends); i += 1) {
    271 		const name = strings::join("::", ver.depends[i]...);
    272 		defer free(name);
    273 		const child = match (explore_deps(&ctx, &stack, &visited, name)) {
    274 		case let index: size => yield index;
    275 		case let start: dep_cycle =>
    276 			const chain = strings::join(" -> ", stack[start..]...);
    277 			defer free(chain);
    278 			fmt::errorln("Dependency cycle detected:", chain)!;
    279 			os::exit(1);
    280 		};
    281 		append(toplevel.depends, child);
    282 	};
    283 
    284 	sort::sort(toplevel.depends, size(size), &cmpsz);
    285 	append(visited, toplevel);
    286 	defer for (let i = 0z; i < len(visited); i += 1) {
    287 		free(visited[i].ident);
    288 		free(visited[i].depends);
    289 	};
    290 
    291 	switch (goal) {
    292 	case deps_goal::TERM =>
    293 		show_deps(&visited);
    294 	case deps_goal::DOT =>
    295 		fmt::println("strict digraph deps {")!;
    296 		for (let i = 0z; i < len(visited); i += 1) {
    297 			for (let j = 0z; j < len(visited[i].depends); j += 1) {
    298 				const child = visited[visited[i].depends[j]];
    299 				fmt::printfln("\t\"{}\" -> \"{}\";", visited[i].ident, child.ident)!;
    300 			};
    301 		};
    302 		fmt::println("}")!;
    303 	case deps_goal::MAKE =>
    304 		abort("-M option not implemented yet");
    305 	};
    306 };
    307 
    308 fn release(cmd: *getopt::command) void = {
    309 	let dryrun = false;
    310 	for (let i = 0z; i < len(cmd.opts); i += 1) {
    311 		let opt = cmd.opts[i];
    312 		switch (opt.0) {
    313 		case 'd' =>
    314 			dryrun = true;
    315 		case => abort();
    316 		};
    317 	};
    318 
    319 	if (len(cmd.args) == 0) {
    320 		getopt::printusage(os::stderr, "release", cmd.help)!;
    321 		os::exit(1);
    322 	};
    323 
    324 	const next = switch (cmd.args[0]) {
    325 	case "major" =>
    326 		yield increment::MAJOR;
    327 	case "minor" =>
    328 		yield increment::MINOR;
    329 	case "patch" =>
    330 		yield increment::PATCH;
    331 	case =>
    332 		yield match (parseversion(cmd.args[0])) {
    333 		case badversion =>
    334 			getopt::printusage(os::stderr, "release", cmd.help)!;
    335 			os::exit(1);
    336 		case let ver: modversion =>
    337 			yield ver;
    338 		};
    339 	};
    340 
    341 	match (do_release(next, dryrun)) {
    342 	case void => void;
    343 	case let err: exec::error =>
    344 		fmt::fatal(exec::strerror(err));
    345 	case let err: errors::error =>
    346 		fmt::fatal(errors::strerror(err));
    347 	case let err: io::error =>
    348 		fmt::fatal(io::strerror(err));
    349 	case let err: fs::error =>
    350 		fmt::fatal(fs::strerror(err));
    351 	case let err: git_error =>
    352 		fmt::fatal("git:", exec::exitstr(err));
    353 	case badversion =>
    354 		fmt::fatal("Error: invalid format string. Hare uses semantic versioning, in the form major.minor.patch.");
    355 	};
    356 };
    357 
    358 fn run(cmd: *getopt::command) void = {
    359 	const build_target = default_target();
    360 	let tags = module::tags_dup(build_target.tags);
    361 	defer module::tags_free(tags);
    362 
    363 	let verbose = false;
    364 	let defines: []str = [];
    365 	defer free(defines);
    366 	let libdir: []str = [];
    367 	defer free(libdir);
    368 	let libs: []str = [];
    369 	defer free(libs);
    370 	for (let i = 0z; i < len(cmd.opts); i += 1) {
    371 		let opt = cmd.opts[i];
    372 		switch (opt.0) {
    373 		case 'v' =>
    374 			verbose = true;
    375 		case 'D' =>
    376 			append(defines, opt.1);
    377 		case 'j' =>
    378 			abort("-j option not implemented yet."); // TODO
    379 		case 'L' =>
    380 			append(libdir, opt.1);
    381 		case 'l' =>
    382 			append(libs, opt.1);
    383 		case 't' =>
    384 			abort("-t option not implemented yet."); // TODO
    385 		case 'T' =>
    386 			tags = match (addtags(tags, opt.1)) {
    387 			case void =>
    388 				fmt::fatal("Error parsing tags");
    389 			case let t: []module::tag =>
    390 				yield t;
    391 			};
    392 		case 'X' =>
    393 			tags = match (deltags(tags, opt.1)) {
    394 			case void =>
    395 				fmt::fatal("Error parsing tags");
    396 			case let t: []module::tag =>
    397 				yield t;
    398 			};
    399 		case =>
    400 			abort();
    401 		};
    402 	};
    403 
    404 	let input = "";
    405 	let runargs: []str = [];
    406 	if (len(cmd.args) == 0) {
    407 		input = os::getcwd();
    408 	} else {
    409 		input = cmd.args[0];
    410 		runargs = cmd.args[1..];
    411 	};
    412 
    413 	if (len(libs) > 0) {
    414 		append(tags, module::tag {
    415 			mode = module::tag_mode::INCLUSIVE,
    416 			name = strings::dup("libc"),
    417 		});
    418 	};
    419 
    420 	const ctx = module::context_init(tags, defines, HAREPATH);
    421 	defer module::context_finish(&ctx);
    422 
    423 	const plan = mkplan(&ctx, libdir, libs, build_target);
    424 	defer plan_finish(&plan);
    425 
    426 	const ver = match (module::scan(&ctx, input)) {
    427 	case let ver: module::version =>
    428 		yield ver;
    429 	case let err: module::error =>
    430 		fmt::fatal("Error scanning input module:",
    431 			module::strerror(err));
    432 	};
    433 
    434 	let depends: []*task = [];
    435 	sched_module(&plan, ["rt"], &depends);
    436 
    437 	for (let i = 0z; i < len(ver.depends); i += 1z) {
    438 		const dep = ver.depends[i];
    439 		sched_module(&plan, dep, &depends);
    440 	};
    441 
    442 	const output = mkfile(&plan, "", "out");
    443 	sched_hare_exe(&plan, ver, output, depends...);
    444 	match (plan_execute(&plan, verbose)) {
    445 	case void => void;
    446 	case !exec::exit_status =>
    447 		fmt::fatalf("{} run: build failed", os::args[0]);
    448 	};
    449 	const cmd = match (exec::cmd(output, runargs...)) {
    450 	case let err: exec::error =>
    451 		fmt::fatal("exec:", exec::strerror(err));
    452 	case let cmd: exec::command =>
    453 		yield cmd;
    454 	};
    455 	exec::setname(&cmd, input);
    456 	exec::exec(&cmd);
    457 };
    458 
    459 fn test(cmd: *getopt::command) void = {
    460 	const build_target = default_target();
    461 	let tags = module::tags_dup(build_target.tags);
    462 	append(tags, module::tag {
    463 		name = strings::dup("test"),
    464 		mode = module::tag_mode::INCLUSIVE,
    465 	});
    466 
    467 	let output = "";
    468 	let verbose = false;
    469 	let defines: []str = [];
    470 	defer free(defines);
    471 	let libdir: []str = [];
    472 	defer free(libdir);
    473 	let libs: []str = [];
    474 	defer free(libs);
    475 	for (let i = 0z; i < len(cmd.opts); i += 1) {
    476 		const opt = cmd.opts[i];
    477 		switch (opt.0) {
    478 		case 'v' =>
    479 			verbose = true;
    480 		case 'D' =>
    481 			append(defines, opt.1);
    482 		case 'j' =>
    483 			abort("-j option not implemented yet."); // TODO
    484 		case 'L' =>
    485 			append(libdir, opt.1);
    486 		case 'l' =>
    487 			append(libs, opt.1);
    488 		case 't' =>
    489 			abort("-t option not implemented yet."); // TODO
    490 		case 'o' =>
    491 			output = opt.1;
    492 		case 'T' =>
    493 			tags = match (addtags(tags, opt.1)) {
    494 			case void =>
    495 				fmt::fatal("Error parsing tags");
    496 			case let t: []module::tag =>
    497 				yield t;
    498 			};
    499 		case 'X' =>
    500 			tags = match (deltags(tags, opt.1)) {
    501 			case void =>
    502 				fmt::fatal("Error parsing tags");
    503 			case let t: []module::tag =>
    504 				yield t;
    505 			};
    506 		case =>
    507 			abort();
    508 		};
    509 	};
    510 
    511 	if (len(libs) > 0) {
    512 		append(tags, module::tag {
    513 			mode = module::tag_mode::INCLUSIVE,
    514 			name = strings::dup("libc"),
    515 		});
    516 	};
    517 
    518 	const ctx = module::context_init(tags, defines, HAREPATH);
    519 	defer module::context_finish(&ctx);
    520 
    521 	const plan = mkplan(&ctx, libdir, libs, build_target);
    522 	defer plan_finish(&plan);
    523 
    524 	let depends: []*task = [];
    525 	sched_module(&plan, ["test"], &depends);
    526 
    527 	let items = match (module::walk(&ctx, ".")) {
    528 	case let items: []ast::ident =>
    529 		yield items;
    530 	case let err: module::error =>
    531 		fmt::fatal("Error scanning source root:",
    532 			module::strerror(err));
    533 	};
    534 
    535 	defer module::walk_free(items);
    536 	for (let i = 0z; i < len(items); i += 1) {
    537 		if (len(items[i]) > 0 && items[i][0] == "cmd") {
    538 			continue;
    539 		};
    540 		match (module::lookup(plan.context, items[i])) {
    541 		case let ver: module::version =>
    542 			if (len(ver.inputs) == 0) continue;
    543 		case module::error =>
    544 			continue;
    545 		};
    546 		sched_module(&plan, items[i], &depends);
    547 	};
    548 
    549 	const have_output = len(output) != 0;
    550 	if (!have_output) {
    551 		output = mkfile(&plan, "", "out");
    552 	};
    553 	sched_ld(&plan, strings::dup(output), depends...);
    554 	match (plan_execute(&plan, verbose)) {
    555 	case void => void;
    556 	case !exec::exit_status =>
    557 		fmt::fatalf("{} test: build failed", os::args[0]);
    558 	};
    559 
    560 	if (have_output) {
    561 		return;
    562 	};
    563 
    564 	const cmd = match (exec::cmd(output, cmd.args...)) {
    565 	case let err: exec::error =>
    566 		fmt::fatal("exec:", exec::strerror(err));
    567 	case let cmd: exec::command =>
    568 		yield cmd;
    569 	};
    570 	exec::setname(&cmd, os::getcwd());
    571 	exec::exec(&cmd);
    572 };
    573 
    574 fn version(cmd: *getopt::command) void = {
    575 	let verbose = false;
    576 	for (let i = 0z; i < len(cmd.opts); i += 1) {
    577 		// The only option is verbose
    578 		verbose = true;
    579 	};
    580 
    581 	fmt::printfln("Hare {}", VERSION)!;
    582 
    583 	if (verbose) {
    584 		fmt::printf("Build tags\t")!;
    585 		const build_target = default_target();
    586 		const tags = build_target.tags;
    587 		for (let i = 0z; i < len(tags); i += 1) {
    588 			const tag = tags[i];
    589 			const inclusive = (tag.mode & module::tag_mode::INCLUSIVE) == 0;
    590 			fmt::printf("{}{}", if (inclusive) '+' else '-', tag.name)!;
    591 		};
    592 		fmt::println()!;
    593 
    594 		if (tty::isatty(os::stdout_file)) {
    595 			// Pretty print
    596 			match (os::getenv("HAREPATH")) {
    597 			case void =>
    598 				const items = strings::split(HAREPATH, ":");
    599 				defer free(items);
    600 				const items = strings::join("\n\t\t", items...);
    601 				defer free(items);
    602 				fmt::printfln("HAREPATH\t{}", items)!;
    603 			case let env: str =>
    604 				fmt::printf("HAREPATH\t")!;
    605 				bufio::flush(os::stdout)!;
    606 				fmt::errorf("(from environment)")!;
    607 				const items = strings::split(env, ":");
    608 				defer free(items);
    609 				const items = strings::join("\n\t\t", items...);
    610 				defer free(items);
    611 				fmt::printfln("\n\t\t{}", items)!;
    612 			};
    613 		} else {
    614 			// Print for ease of machine parsing
    615 			const val = match (os::getenv("HAREPATH")) {
    616 			case void =>
    617 				yield HAREPATH;
    618 			case let env: str =>
    619 				yield env;
    620 			};
    621 			fmt::printfln("HAREPATH\t{}", val)!;
    622 		};
    623 	};
    624 };