hare

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

subcmds.ha (13914B)


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