From bda639aec7f3368c5001e07287807b81b8352f98 Mon Sep 17 00:00:00 2001 From: Nathan DECHER Date: Sun, 10 May 2020 16:05:47 +0200 Subject: [PATCH] initial commit --- .gitignore | 3 ++ Build.moon | 33 ++++++++++++ LICENSE | 9 ++++ Makefile | 33 ++++++++++++ README.md | 92 +++++++++++++++++++++++++++++++ moonbuild.moon | 132 +++++++++++++++++++++++++++++++++++++++++++++ util.moon | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 445 insertions(+) create mode 100644 .gitignore create mode 100644 Build.moon create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100755 moonbuild.moon create mode 100644 util.moon diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac66fe4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/*.lua +/*.lua.c +/moonbuild diff --git a/Build.moon b/Build.moon new file mode 100644 index 0000000..35d1c8d --- /dev/null +++ b/Build.moon @@ -0,0 +1,33 @@ +SOURCES_MOON = wildcard '*.moon' +exclude SOURCES_MOON, 'Build.moon' +OUT_LUA = patsubst SOURCES_MOON, '%.moon', '%.lua' +BINARY = 'moonbuild' +MAIN = "#{BINARY}.moon" +MAIN_LUA = patsubst MAIN, '%.moon', '%.lua' +OUT_C = patsubst MAIN, '%.moon', '%.lua.c' +PREFIX = env 'PREFIX', '/usr/local' + +default public target 'all', deps: BINARY + +public target 'install', deps: 'moonbuild', in: 'moonbuild', fn: => + -install @infile, "#{PREFIX}/bin" + +public target 'clean', fn: => + -rm '-f', OUT_LUA + -rm '-f', OUT_C + +public target 'mrproper', deps: 'clean', fn: => + -rm '-f', BINARY + +public target 'info', fn: => + #echo "Moonscript sources:", SOURCES_MOON + #echo "Compiled lua:", OUT_LUA + #echo "Binary:", BINARY + +target BINARY, out: {BINARY, OUT_C}, in: OUT_LUA, deps: OUT_LUA, fn: => + -luastatic MAIN_LUA, OUT_LUA, '-I/usr/include/lua5.3', '-llua5.3' + +foreach OUT_LUA, (file) -> + source=patsubst file, '%.lua', '%.moon' + target file, in: source, out: file, fn: => + -moonc @infile diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..277a56e --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2020 Codinget + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e0b8133 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +SOURCES_MOON := $(wildcard *.moon) +SOURCES_MOON := $(filter-out Build.moon, $(SOURCES_MOON)) +OUT_LUA := $(foreach source, $(SOURCES_MOON), $(patsubst %.moon, %.lua, $(source))) +BINARY := moonbuild +MAIN := $(BINARY).moon +MAIN_LUA := $(patsubst %.moon, %.lua, $(MAIN)) +OUT_C := $(patsubst %.moon, %.lua.c, $(MAIN)) +PREFIX ?= /usr/local + +.PHONY: all install clean mrproper info + +all: $(BINARY) + +install: moonbuild + install $^ $(PREFIX)/bin + +clean: + rm -f $(OUT_LUA) + rm -f $(OUT_C) + +mrproper: clean + rm -f $(BINARY) + +info: + @echo "Moonscript sources:" $(SOURCES_MOON) + @echo "Compiled lua:" $(OUT_LUA) + @echo "Binary:" $(BINARY) + +$(BINARY): $(OUT_LUA) + luastatic $(MAIN_LUA) $(OUT_LUA) -I/usr/include/lua5.3 -llua5.3 + +%.lua: %.moon + moonc $^ diff --git a/README.md b/README.md new file mode 100644 index 0000000..77d6ea5 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Moonbuild +Because `make` is painful to use, and build scripts are too slow. Moonbuild aims to be a good compromise. + +You should probably use [`tup`](http://gittup.org/tup/) instead if you want a good build system. + +## How does it work? +Basically like `make`, but in [Moonscript](https://moonscript.org) and with explicit ordering. See its `Build.moon` for examples (and you can compare it with the `Makefile`, both do the same thing). + +## Why moonscript? +It's fast, based on lua, and it's easy to write DSLs with it, so the build instructions can be readable. Also, it's a full programming language, so there are no arbitrary restrictions. + +## How do I install it? +- First, you'll need Lua 5.2 (untested) or 5.3 and [LuaRocks](https://luarocks.org) +- Then, you'll need `moonscript`, `argparse` and `luastatic`, which you can get from `luarocks` +- Now, you can simply `make` Moonbuild, or build it with itself with `moon moonbuild.moon` +- You're now ready to install it, with `sudo make install` or `sudo ./moonbuild install` + +## Now, how do I use it? +First, you'll need a `Build.moon`, `Buildfile.moon`, `Build` or `Buildfile` in the root of your project. +Then, you'll need a few `target`s, and ideally a `default target` (or the default target will be `all`). `public target`s will be listed by `moonbuild -l`. +To execute a command, you can use either `-cmd` or `#cmd` (the former will print it before executing it, the later won't). + +### `[default] [public] target [deps: ] [in: ] [out: ] [fn: ]` +Define a new target, and give it a list of depenancies, inputs, outputs and a function to run to build it. + +`deps`, `in` and `out` can be either strings or tables. `name` must be a string and `code` must be a function, that will be given a table with the following fields: +- `name`: the name of the target +- `ins`: the table of inputs +- `infile`: the first input +- `outs`: the table of outputs +- `outfile`: the first output + +### `wildcard ` +Returns a table with all the matching files. Valid wildcards contain either `**`, which can be expanded by any characters, including '/', or `*`, which cannot be expanded by `/`. Wildcards can only contain one `**` or `*`. + +`wc` must be a string + +### `exclude [...]` +Removes all exclusions from the given list, and returns it. + +`list` must be a table, and `exclusions` can be any type + +### `patsubst ` +If the string matches `patt`, makes it match `subst` instead. If `str` is a table, it is recursively applied to all array values. + +Patterns are in the format `[prefix]%[suffix]`, with the `%` representing any sequence of characters, including `/`. + +`str`, `pat` and `subst` must be strings + +### `foreach ` +Applies `code` to every element of `table`, and returns the resulting table. + +`table` must be a table, and `code` a function. + +### `min|max
` +Returns either the min or max value of the given table. + +`table` must be a table + +### `first
` +Returns the first value of the table that verifies the given condition. + +`table` must be a table, and `code` a function + +### `flatten
` +Flattens a table so that it has exactly one dimension. + +`table` can be anything + +### `insert|unpack|concat` +The functions, imported from the `table` library. + +### `mtime ` +Returns the modification time of `file`, or `nil` if it doesn't exist. + +`file` must be a string + +### `exists ` +Returns `true` if the file exists, `false` otherwise. + +`file` must be a string + +### `run [ [print: ] [error: ]]` +Runs the given command with the given arguments. If `print` is truthy, prints the command before executing it, and if `error` is truthy, crashes if the command fails. Returns a boolean which is true if the command ended with success, and a number which is the return code. + +`cmd` must be a string, `args` must be a table, which can contain either strings or other tables. `raw: ` is a special kind of argument that will not be escaped. `print` and `error` can be anything, but booleans or nil are recommended + +### `popen [ [print: ]]` +Same as `run`, but returns a `io.popen` handle. + +## License +MIT diff --git a/moonbuild.moon b/moonbuild.moon new file mode 100755 index 0000000..cded531 --- /dev/null +++ b/moonbuild.moon @@ -0,0 +1,132 @@ +#!/usr/bin/env moon + +argparse=require 'argparse' + +require 'moonscript' +import loadfile from require 'moonscript.base' +import truncate_traceback, rewrite_traceback from require 'moonscript.errors' +import trim from require 'moonscript.util' + +util=require 'util' +import exists, mtime, run, min, max, first, flatten from util + +import insert, concat from table + +parser=argparse 'moonbuild' +parser\argument('targets', "Targets to run")\args '*' +parser\flag '-a --noskip', "Always run targets" +parser\flag '-l --list', "List available targets" +args=parser\parse! + +-- util functions +loadwithscope= (file, scope) -> + fn=loadfile file + dumped=string.dump fn + load dumped, file, 'b', scope +pcall= (fn, ...) -> + rewrite=(err) -> + trace=debug.traceback '', 2 + trunc=truncate_traceback trim trace + rewrite_traceback trunc, err + xpcall fn, rewrite, ... + +-- command object +-- represents a command that can be called +class Command + new: (@cmd, ...) => + @args={...} + + __unm: => @run error: true, print: true + __len: => @run error: true + __tostring: => @cmd + + run: (params) => run @cmd, @args, params + @run: (...) => -@ ... + +-- build object +-- represents a target +class BuildObject + all={} + + @build: (name) => + target=all[name] or error "No such target: #{name}" + target\build! + + new: (@name, @outs={}, @ins={}, @deps={}, @fn= =>) => + @skip=false + error "Duplicate build name #{@name}" if all[@name] + all[@name]=@ + + build: => + return if @skip + error "Can't build #{@name}: cyclic dependancy" if @cycle + @cycle=true + for depname in *@deps + dep=all[depname] or error "Can't build #{@name}: missing dependancy #{depname}" + dep\build! + return unless @shouldbuild! + + print "Building #{@name}" + ok, err=pcall -> + @.fn ins: @ins, outs: @outs, infile: @ins[1], outfile: @outs[1], name: @name + error "Can't build #{@name}: lua error\n#{err}" unless ok + for f in *@outs + error "Can't build #{@name}: output file #{f} not created" unless exists f + @skip=true + + shouldbuild: => + return true if args.noskip + return true if #@ins==0 or #@outs==0 + + itimes=[mtime f for f in *@ins] + for i=1, #@ins + error "Can't build #{@name}: missing inputs" unless itimes[i] + + otimes=[mtime f for f in *@outs] + for i=1, #@outs + return true if not otimes[i] + + (max itimes)>(min otimes) + +error "Need Lua >=5.2" if setfenv + +targets={} +defaulttarget='all' + +buildscope= + default: (target) -> + defaulttarget=target.name + target + public: (target) -> + insert targets, target.name + target + target: (name, params) -> + BuildObject name, (flatten params.out), (flatten params.in), (flatten params.deps), params.fn +buildscope[k]=fn for k, fn in pairs util + +setmetatable buildscope, + __index: (k) => + global=rawget _G, k + return global if global + (...) -> Command k, ... + +file=first {'Build.moon', 'Buildfile.moon', 'Build', 'Buildfile'}, exists +error "No Build.moon or Buildfile found" unless file +buildfn=loadwithscope file, buildscope +ok, err=pcall buildfn +unless ok + if err + io.stderr\write err, '\n' + else + io.stderr\write "Unknown error\n" + os.exit 1 + +if args.list + io.write "Available targets:\n" + io.write "\t#{concat targets, ', '}\n" + os.exit 0 + +if #args.targets==0 + BuildObject\build defaulttarget +for target in *args.targets + BuildObject\build target diff --git a/util.moon b/util.moon new file mode 100644 index 0000000..07ca90c --- /dev/null +++ b/util.moon @@ -0,0 +1,143 @@ +import attributes, dir from require 'lfs' + +import insert, concat from table +unpack or=table.unpack + +-- min and max of table +max= (t) -> + m=t[1] + for i=2, #t + v=t[i] + m=v if v>m + m +min= (t) -> + m=t[1] + for i=2, #t + v=t[i] + m=v if v + [fn e for e in *tab] + +first= (tab, fn) -> + for e in *tab + return e if fn e + +exclude= (tab, ...) -> + i=1 + while i<=#tab + removed=false + for j=1, select '#', ... + if tab[i]==select j, ... + table.remove tab, i + removed=true + break + i+=1 unless removed + tab + +flatten= (tab) -> + return {tab} if (type tab)!='table' + out={} + for e in *tab + if (type e)=='table' and e[1] + insert out, v for v in *flatten e + else + insert out, e + out + +-- file functions +mtime= (f) -> + a=attributes f + a and a.modification +exists= (f) -> + (attributes f)!=nil + +-- command functions +escapecmdpart= (p) -> + if (type p)=='table' + return p.raw if p.raw + return concat [escapecmdpart part for part in *p], ' ' + return p if p\match '^[a-zA-Z0-9_./-]+$' + '"'..p\gsub('\\', '\\\\')\gsub('"', '\\"')..'"' +escapecmd= (c, args={}) -> + c=escapecmdpart c + c..=' '..escapecmdpart a for a in *args + c +run= (c, args, params={}) -> + escaped=escapecmd c, args + print escaped if params.print + ret, _, code=os.execute escaped + ret, code=ret==0, ret if (type ret)=='number' + error "#{c} failed with code #{code}" if params.error and not ret + ret, code +popen= (c, args, mode='r', params={}) -> + escaped=escapecmd c, args + print escaped if params.print + io.popen escaped, mode + +-- file matcher +wildcard= (pattern) -> + prefix, suffix=pattern\match '^(.*)%*%*(.*)$' + if prefix + fd=popen 'find', {(raw: '*'), '-name', "*#{suffix}"} + found={} + for line in fd\lines! + insert found, line if (line\sub 1, #prefix)==prefix + fd\close! + return found + + directory, prefix, suffix=pattern\match '^([^/]*)/(.*)%*(.*)$' + if directory + found={} + for file in dir directory + if (file\sub 1, #prefix)==prefix and (file\sub -#suffix)==suffix + insert found, "#{directory}/#{file}" + return found + + prefix, suffix=pattern\match '^(.*)%*(.*)$' + if prefix + found={} + for file in dir '.' + if (file\sub 1, #prefix)==prefix and (file\sub -#suffix)==suffix + insert found, file + return found + + error "Invalid wildcard pattern: #{pattern}" + +-- string pattern +patsubst= (str, pattern, replacement) -> + return [patsubst s, pattern, replacement for s in *str] if (type str)=='table' + prefix, suffix=pattern\match '^(.*)%%(.*)$' + error "Invalid pattern #{pattern}" unless prefix + reprefix, resuffix=replacement\match '^(.*)%%(.*)$' + error "Invalid replacement pattern #{pattern}" unless reprefix + + if (str\sub 1, #prefix)==prefix and (str\sub -#suffix)==suffix + return reprefix..(str\sub #prefix+1, -#suffix-1)..resuffix + str + +env= (key, def) -> + (os.getenv key) or def + +{ + -- table functions + :min, :max + :foreach + :first + :insert, :unpack, :concat + :exclude + :flatten + + -- file functions + :wildcard + :mtime, :exists + + -- command functions + :run, :popen + + -- string functions + :patsubst + :env +}