Skip to content

Commit

Permalink
initial release v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
heinthanth committed Mar 31, 2022
0 parents commit a1f80ca
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.coffee]
tab_width = 4
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# deno build
bin
tmp

# bootstrap cache
tools/tmp*
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"deno.enable":true,"deno.lint":true,"deno.unstable":true}
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
COPYRIGHT 2022 HEIN THANT MAUNG MAUNG

REDISTRIBUTION AND USE IN SOURCE AND BINARY FORMS, WITH OR WITHOUT MODIFICATION, ARE PERMITTED PROVIDED THAT THE FOLLOWING CONDITIONS ARE MET:

1. REDISTRIBUTIONS OF SOURCE CODE MUST RETAIN THE ABOVE COPYRIGHT NOTICE, THIS LIST OF CONDITIONS AND THE FOLLOWING DISCLAIMER.

2. REDISTRIBUTIONS IN BINARY FORM MUST REPRODUCE THE ABOVE COPYRIGHT NOTICE, THIS LIST OF CONDITIONS AND THE FOLLOWING DISCLAIMER IN THE DOCUMENTATION AND/OR OTHER MATERIALS PROVIDED WITH THE DISTRIBUTION.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# XueRun ( 雪 run )

Just a make-like task runner but with more power! It's written in CoffeeScript + Deno.

## How to Build

First, clone this repo. Then, run "tools/bootstrap.ts"

```shell
deno run tools/bootstrap.ts
```

## License

XueRun is licensed under BSD-2-Clause. For more, see [LICENSE](LICENSE).
104 changes: 104 additions & 0 deletions src/core.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as eta from "https://deno.land/x/[email protected]/mod.ts"

eta.configure
tags: ["{{", "}}"]
useWith: true
parse: { exec: "#", raw: "%", interpolate: "" }

# magic happens here
export runRecipe = (rc, recipe, options, recon, asIngredient) ->
if not (rc.hasOwnProperty(recipe))
console.error("\nxuerun: oops, recipe '#{recipe}' is not in .xuerun tasks!\n")
Deno.exit(1)
# resolve dependencies
currentRecipe = rc[recipe]

currentOption = {}
if typeof currentRecipe.passEnv == "boolean" and currentRecipe.passEnv
currentOption = { ...currentOption, ...Deno.env.toObject() }
else if Array.isArray(currentRecipe.passEnv)
currentOption = { ...currentOption }
currentRecipe.passEnv.forEach (env) -> currentOption[env] = Deno.env.get(env)
else ### do pass env ###

if asIngredient then currentOption = { ...currentOption, ...options }
else if typeof currentRecipe.passCLI == "boolean" and currentRecipe.passCLI
currentOption = { ...currentOption, ...options }
else if Array.isArray(currentRecipe.passCLI)
# since current task is main task ( not ingredient ), options point to CLI option
currentOption = { ...currentOption }
currentRecipe.passCLI.forEach (opt) -> currentOption[opt] = options[opt] || ""
else ### don't pass options ###

dependencies = if typeof currentRecipe.dependencies ==
"string" then currentRecipe.dependencies.split(" ") else currentRecipe.dependencies

dependencies.forEach (dep) ->
# won't pass options
if typeof dep == "string" then return runRecipe(rc, dep, {})

depOption = { ...dep.options }
if typeof dep.passParentOptions == "boolean" and dep.passParentOptions
depOption = { ...currentOption, ...depOption }
else if Array.isArray(dep.passParentOptions)
dep.passParentOptions.forEach (opt) -> depOption[opt] = currentOption[opt]
else ### do pass parent option ###

# resolve ( compile ) option value with current option
depOptionToBePassed = {}
Object.entries(depOption).forEach ([option, value]) ->
unless typeof value == "string" then return depOptionToBePassed[option] = value
try depOptionToBePassed[option] = eta.render(value, currentOption)
catch err
console.error("\nxuerun: oops, something went wrong while reading options.\nError:",
err.message, "\n")
Deno.exit(1)
runRecipe(rc, dep.name, depOptionToBePassed, recon, true)

# make main recipe
commands = currentRecipe.command
(if typeof commands == "string" then [commands] else commands)
.map (cmdOption) ->
try
if typeof cmdOption == "string" then return eta.render(cmdOption, currentOption)
else return {...cmdOption, cmd: eta.render(cmdOption.cmd, currentOption)}
catch err
console.error(
"\nxuerun: oops, something went wrong while reading command.\nError:",
err.message, "\n")
Deno.exit(1)
.forEach (cmdOption) ->
commandToRun = [
(if typeof cmdOption == "object" and
cmdOption.shell then cmdOption.shell else currentRecipe.shell), "-c",
if typeof cmdOption == "string" then cmdOption else cmdOption.cmd ]

# if recon, just show command
if recon then return console.info(commandToRun)

# run command
preparedEnv = {}
Object.entries(currentOption).forEach ([k, v]) ->
preparedEnv[k] = switch
when v == null then ""
when typeof v == "undefined" then ""
else v.toString()

commandProcess = null
try
commandProcess = Deno.run
cmd: commandToRun
stdin: "inherit"
stdout: "inherit"
stderr: "inherit"
clearEnv: true
env: preparedEnv
catch err
console.error("\nxuerun: Something went wrong while running command", commandToRun)
console.error("Error:", err.message, "\n")

if commandProcess == null then Deno.exit(1)
status = await commandProcess.status()
if status.code != 0
console.error("\nxuerun: command exit with exit code:", status.code, "\n")
Deno.exit(status.code)
30 changes: 30 additions & 0 deletions src/schema.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { create, defaulted, optional, union, object, record,
array, string, number, boolean, validate } from "https://esm.sh/superstruct";

XueRunUserCmd = union([string(), object({ shell: optional(string()), cmd: string() })]);

XueRunIngredient$option = union([string(), number(), boolean()])
export XueRunIngredient = object
name: string(),
options: defaulted(optional(record(string(), XueRunIngredient$option)), () => ({})),
passParentOptions: defaulted(optional(union([boolean(), array(string())])), () => !1),

getCurrentSH = -> if Deno.build.os == "windows" then "cmd" else "sh"

XueRunRecipe$dependencies = union([string(), array(union([string(), XueRunIngredient]))])
export XueRunRecipe = object
description: defaulted(optional(string()), () => ""),
shell: defaulted(optional(string()), () => Deno.env.get("SHELL") || getCurrentSH()),
command: defaulted(optional(union([string(), array(XueRunUserCmd)])), () => ""),
passEnv: defaulted(optional(union([boolean(), array(string())])), () => !1),
passCLI: defaulted(optional(union([boolean(), array(string())])), () => !1),
dependencies: defaulted(optional(XueRunRecipe$dependencies), () => []),

export XueRunConfiguration = record(string(), XueRunRecipe)

createConfiguration = (userConfig) ->
[err, data] = validate userConfig, XueRunConfiguration
if err then throw err
return create data, XueRunConfiguration

export default createConfiguration
95 changes: 95 additions & 0 deletions src/xuerun.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { parse } from "https://deno.land/[email protected]/flags/mod.ts";
import { parse as parseYAML } from "https://deno.land/[email protected]/encoding/yaml.ts"
import { YAMLError } from "https://deno.land/[email protected]/encoding/_yaml/error.ts"
import { printf } from "https://deno.land/[email protected]/fmt/printf.ts"
import { StructError } from "https://esm.sh/superstruct"
import { runRecipe } from "./core.coffee"
import createConfiguration from "./schema.coffee"

loadXueRunTasks = (path) ->
try
content = Deno.readTextFileSync(path)
return createConfiguration(parseYAML(content))
catch error
if error instanceof YAMLError or error instanceof StructError
console.error(
"\nxuerun: oops, invalid .xuerun tasks.\nError:", error.message, "\n")
else console.error(
"\nxuerun: oops, can't read .xuerun tasks.\nError:", error.message, "\n")
Deno.exit(1)

printVersion = () ->
console.log() # print padding
ascii = [
'y88b / 888~-_ '
' y88b / 888 888 e88~~8e 888 \\ 888 888 888-~88e'
' y88b/ 888 888 d888 88b 888 | 888 888 888 888'
' /y88b 888 888 8888__888 888 / 888 888 888 888'
' / y88b 888 888 y888 , 888_-~ 888 888 888 888'
'/ y88b "88_-888 "88___/ 888 ~-_ "88_-888 888 888'].join("\n")
console.info("%s\n", ascii)
printf("XueRun v%s ( %s / %s )\n", "0.1.0", Deno.build.os, Deno.build.arch)
currentYear = new Date().getFullYear()
printf("(c) 2022%s Hein Thant Maung Maung. Licensed under BSD-2-CLAUSE.\n\n",
if currentYear == 2022 then "" else " - #{currentYear}")

printHelp = (shouldPrintVersion = true) ->
if shouldPrintVersion then printVersion()
helpStrings = [
"General:",
" xuerun [tasks]... [options]...",
"",
"Options:",
" -t, --tasks path to xuerun tasks ( default: tasks.xuerun ).",
" -n, --recon do nothing, print commands."
"",
" -v, --version print xuerun version and others.",
" -h, --help print this help message.",
"",
"For docs, usage, etc., visit https://github.com/heinthanth/xuerun."].join("\n")
console.info(helpStrings, "\n")

actionKind =
RUN_RECIPE: "RUN_RECIPE"
PRINT_HELP: "PRINT_HELP"
PRINT_VERSION: "PRINT_VERSION"

parseCmdOption = () ->
{'_': positional, '--': cliRest, ...options} = parse Deno.args, { "--": true }
userOption =
action: actionKind.RUN_RECIPE,
recon: !1,
recipes: []
options: {}
tasksPath: "tasks.xuerun"

# parse options
Object.entries(options).forEach ([k, v]) -> switch k
when "h", "help" then userOption.action = actionKind.PRINT_HELP
when "v", "version" then userOption.action = actionKind.PRINT_VERSION
when "t", "tasks" then userOption.tasksPath =
if typeof v == "string" or typeof v == "number" then v.toString() else "tasks.xuerun"
when "n", "recon" then userOption.recon = true

# remove default CLI arguments
{h, v, t, n, help, version, tasks, recon, ...restCLIargs} = options
# parse options passed with -- --something
{'_': _restPositional, ...optionsFromCLIrestOptions} = parse(cliRest)
# combine options
userOption.options = { ...restCLIargs, ...optionsFromCLIrestOptions }
return { ...userOption, recipes: positional }

programMain = () ->
{recipes, tasksPath, action, options, recon} = parseCmdOption()
if action == actionKind.PRINT_HELP then return printHelp()
if action == actionKind.PRINT_VERSION then return printVersion()

# load and run
xueRunRc = loadXueRunTasks(tasksPath)
if recipes.length == 0
if xueRunRc.hasOwnProperty("all") then return runRecipe(xueRunRc, recipes, userOption)
else console.error("\nxuerun: oops, no recipe given, nothing to do!\n"); Deno.exit(1)
recipes.forEach (recipe) -> runRecipe(xueRunRc, recipe, options, recon, !1)

# call main function
if import.meta.main then programMain()
19 changes: 19 additions & 0 deletions tools/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run --allow-env

import { compile } from "https://esm.sh/coffeescript";
import { join, parse, format } from "https://deno.land/[email protected]/path/mod.ts";

const __dirname = new URL(".", import.meta.url).pathname;
const compiler = join(__dirname, "build.coffee");
const { base: _base, ext: _ext, ...rest } = parse(compiler);
const compilerOut = format({ ...rest, name: "tmp_", ext: ".js" });

await Deno.writeTextFile(compilerOut, compile(await Deno.readTextFile(compiler)));

const cmd = ["deno", "run", "--allow-read",
"--allow-write", "--allow-env", "--allow-run", compilerOut];
const p = Deno.run({ cmd, stderr: "inherit" });

const processStatus = await p.status();
await Deno.remove(compilerOut);
(processStatus.code != 0) && Deno.exit(processStatus.code);
27 changes: 27 additions & 0 deletions tools/build.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { compile } from "https://esm.sh/coffeescript"
import { walkSync, ensureDirSync, emptyDir } from "https://deno.land/[email protected]/fs/mod.ts"
import { join, parse, format } from "https://deno.land/[email protected]/path/mod.ts"

__dirname = new URL(".", import.meta.url).pathname
srcPath = join(__dirname, "..", "src")
distPath = join(__dirname, "..", "tmp")
binPath = join(__dirname, "..", "bin", "xuerun")
mainMod = join(distPath, "xuerun.js")

await emptyDir(distPath)

Array(...walkSync(srcPath)).forEach (p) ->
dist = p.path.replace(srcPath, distPath)
if (p.isDirectory) then return ensureDirSync(p.path)
content = Deno.readTextFileSync(p.path)
{ base: _base, ext: _ext, ...d } = parse(dist)
compiledCode = compile(content, { bare: true }).replace(/\.coffee/g, ".js")
Deno.writeTextFileSync(format({ ...d, ext: ".js" }), compiledCode)

cmd = ["deno", "compile", "--allow-read", "--allow-write",
"--allow-env", "--allow-run", "--unstable", "--output", binPath, mainMod]
p = Deno.run({ cmd })

processStatus = await p.status()
await Deno.remove(distPath, { recursive: true });
(processStatus.code != 0) and Deno.exit(processStatus.code);

0 comments on commit a1f80ca

Please sign in to comment.