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 };