import { bold, red } from "https://deno.land/std@v0.37.1/fmt/colors.ts";import { existsSync, walkSync } from "https://deno.land/std@v0.37.1/fs/mod.ts";import * as path from "https://deno.land/std@v0.37.1/path/mod.ts";
export class DrakeError extends Error { constructor(message?: string) { super(message); this.name = "DrakeError"; }}
type Env = { [name: string]: any; "--tasks": string[] };
export const env: Env = { "--tasks": [] };
export function parseEnv(args: string[], env: Env): void { let arg: string | undefined; while (!!(arg = args.shift())) { const match = arg.match(/^([a-zA-Z]\w*)=(.*)$/); if (match) { env[match[1]] = match[2]; continue; } switch (arg) { case "-a": case "--always-make": env["-a"] = true; env["--always-make"] = true; break; case "-d": case "--directory": arg = args.shift(); if (arg === undefined) { abort("missing --directory option value"); } env["--directory"] = arg; break; case "-f": case "--drakefile": arg = args.shift(); if (arg === undefined) { abort("missing --drakefile option value"); } env["--drakefile"] = arg; break; case "-h": case "--help": env["--help"] = true; break; case "-l": case "--list-tasks": env["--list-tasks"] = true; break; case "-L": env["--list-all"] = true; break; case "-n": case "--dry-run": env["--dry-run"] = true; break; case "-q": case "--quiet": env["--quiet"] = true; break; case "--version": env["--version"] = true; break; default: if (arg.startsWith("-")) { abort(`illegal option: ${arg}`); } env["--tasks"].push(arg); break; } }}
export function abort(message: string): never { if (env["--abort-exits"]) { console.error(`${red(bold("drake error:"))} ${message}`); Deno.exit(1); } else { throw new DrakeError(message); }}
export function log(message: string): void { if (!env["--quiet"]) { console.log(message); }}
export function quote(values: string[], sep: string = " "): string { values = values.map(value => `"${value.replace(/"/g, '\\"')}"`); return values.join(sep);}
export function readFile(filename: string): string { return new TextDecoder("utf-8").decode(Deno.readFileSync(filename));}
export function writeFile(filename: string, text: string): void { Deno.writeFileSync(filename, new TextEncoder().encode(text));}
export function updateFile( filename: string, find: RegExp, replace: string): void { writeFile(filename, readFile(filename).replace(find, replace));}
export function touch(...files: string[]): void { for (const file of files) { const dir = path.dirname(file); if (!existsSync(dir)) { Deno.mkdirSync(dir, { recursive: true }); } Deno.openSync(file, "w").close(); }}
export function outOfDate(target: string, prereqs: string[]): boolean { if (!existsSync(target)) { return true; } const targetStat = Deno.statSync(target); for (const prereq of prereqs) { const prereqStat = Deno.statSync(prereq); if (!targetStat.modified || !prereqStat.modified) { continue; } if (targetStat.modified < prereqStat.modified) { return true; } } return false;}
export function isNormalTask(name: string): boolean { return /^\w[\w-]*$/.test(name);}
export function isFileTask(name: string): boolean { return !isNormalTask(name);}
export function normalizePath(name: string): string { name = path.normalize(name); if (isNormalTask(name)) { name = "." + path.sep + name; } return name;}
export function normalizeTaskName(name: string): string { name = name.trim(); if (name === "") { abort("blank task name"); } if (path.isGlob(name)) { abort(`wildcard task name not allowed: ${name}`); } if (isFileTask(name)) { name = normalizePath(name); } return name;}
export function normalizePrereqs(prereqs: string[]): string[] { const result: string[] = []; for (let prereq of prereqs) { prereq = prereq.trim(); if (prereq === "") { abort("blank prerequisite name"); } if (!isFileTask(prereq)) { result.push(prereq); } else if (path.isGlob(prereq)) { result.push(...glob(prereq)); } else { result.push(normalizePath(prereq)); } } return result;}
export function glob(...patterns: string[]): string[] { function glob1(pattern: string): string[] { const globOptions = { extended: true, globstar: true }; pattern = path.normalizeGlob(pattern, globOptions); let root = path.dirname(pattern); while (root !== "." && path.isGlob(root)) { root = path.dirname(root); } const regexp = path.globToRegExp(pattern, globOptions); const iter = walkSync(root, { match: [regexp], includeDirs: false }); return Array.from(iter, info => info.filename); } let result: string[] = []; for (const pattern of patterns) { result = [...result, ...glob1(pattern)]; } return [...new Set(result)].map(p => normalizePath(p)).sort();}
function shArgs(command: string): [string[], string | undefined] { let cmdArgs: string[]; let cmdFile: string | undefined; if (Deno.build.os === "win") { cmdFile = Deno.makeTempFileSync( { prefix: "drake_", suffix: ".cmd" } ); writeFile(cmdFile, `@echo off\n${command}`); cmdArgs = [cmdFile]; } else { const shellExe = Deno.env("SHELL")!; if (!shellExe) { abort(`cannot locate shell: missing SHELL environment variable`); } cmdArgs = [shellExe, "-c", command]; } return [cmdArgs, cmdFile];}
export interface ShOpts { cwd?: string; env?: { [key: string]: string }; stdout?: Deno.ProcessStdio; stderr?: Deno.ProcessStdio;}
export async function sh(commands: string | string[], opts: ShOpts = {}) { if (typeof commands === "string") { commands = [commands]; } const tempFiles: string[] = []; const processes: Deno.Process[] = []; const results: Deno.ProcessStatus[] = []; try { for (const cmd of commands) { let cmdArgs: string[]; let cmdFile: string | undefined; [cmdArgs, cmdFile] = shArgs(cmd); if (cmdFile) tempFiles.push(cmdFile); const p = Deno.run({ cmd: cmdArgs, cwd: opts.cwd, env: opts.env, stdout: opts.stdout ?? "inherit", stderr: opts.stderr ?? "inherit" }); processes.push(p); } results.push(...await Promise.all(processes.map(p => p.status()))); } finally { for (const p of processes) { p.close(); } } for (const f of tempFiles) { Deno.removeSync(f); } for (const i in results) { const cmd = commands[i]; const code = results[i].code; if (code === undefined) { abort(`sh: ${cmd}: undefined exit code`); } if (code !== 0) { abort(`sh: ${cmd}: error code: ${code}`); } }}
export type ShOutput = { code: number | undefined; output: string; error: string;};
export interface ShCaptureOpts extends ShOpts { input?: string;}
export async function shCapture( command: string, opts: ShCaptureOpts = {}): Promise<ShOutput> { let cmdArgs: string[]; let cmdFile: string | undefined; [cmdArgs, cmdFile] = shArgs(command); const p = Deno.run({ cmd: cmdArgs, cwd: opts.cwd, env: opts.env, stdin: opts.input !== undefined ? "piped" : undefined, stdout: opts.stdout ?? "piped", stderr: opts.stderr ?? "piped" }); let status: Deno.ProcessStatus; let outputBytes, errorBytes: Uint8Array; try { if (p.stdin) { await p.stdin.write(new TextEncoder().encode(opts.input)); p.stdin.close(); } [status, outputBytes, errorBytes] = await Promise.all( [ p.status(), p.stdout ? p.output() : Promise.resolve(new Uint8Array()), p.stderr ? p.stderrOutput() : Promise.resolve(new Uint8Array()) ] ); } finally { p.close(); } if (cmdFile) Deno.removeSync(cmdFile); return { code: status.code, output: new TextDecoder().decode(outputBytes), error: new TextDecoder().decode(errorBytes) };}