Biild.ik |
|
---|---|
|
|
Ioke is a very powerful language, running on the JVM and incorporating ideas from languages like Io, Ruby, and Lisp. In this tutorial we will be making Biild, a simple build system much like the Ruby make, Rake. I’ll also assume you’re at least a little familiar with Ruby and Rake, as I’ll be drawing some parallels between them and Ioke. Before diving in, you should probably read the Ioke guide. The tutorial will touch on many of Ioke’s features: most everything is a message send, implicit return, destructuring macros, the condition system, and a little bit of functional programming. Biild itself is pretty simple. It will support task dependencies, descriptions, and namespacing, but nothing else. The bare necessities. |
|
To make things easier on ourselves later, we’ll start out by making a helper method to return the correct path separator of the platform Biild is on. |
FileSystem separator = method( |
Since control structures in Ioke are just messages, not special language
constructs, they can return values just like any another message.
The |
case(System windows?,
true, "\\",
false, "/")) |
The Biild Object |
|
The |
Biild = Origin mimic do( |
The current namespace isn’t stored as a string, but a list of names. Each element in the list is one nesting level of namespacing. That way, as namespaces are entered and exited, they are actually pushed and popped to the list of namespaces which is treated as a stack. |
ns = [] |
Biild keeps an internal list of |
tasks = []) |
The Task Object |
|
A Biild task is just a named block of Ioke code, exactly like Rake tasks. There is nothing new here. A task can have dependencies, a description, and a body. |
Task = Origin mimic do( |
When a |
invoke = method( |
First, each dependency of the task is invoked if it hasn’t been already. If any dependency fails, Biild immediately stops processing. |
@dependencies each(d, |
A task stores its dependencies as a list of task names, but a
|
dep = Biild tasks select(t, t name asText == d asText) first |
The |
if(dep cell?(:success) not, dep invoke)
if(dep success not,
error!("Dependency `#{dep name}' of `#{@name}' failed"))) |
Secondly, we take advantage of Ioke’s condition system to invoke tasks in a controlled environment. The condition system is sort of similar to exception handling, but it would do you good to read up about them in the Ioke guide if you haven’t already. The |
@success = if(bind( |
This rescue call will rescue any condition raised by the code in
the second parameter to |
rescue(fn(c, c report println. false)), |
The body of the task is finally evaluated here.
It is sent to |
@body evaluateOn(Ground) |
Depending on what is returned to the |
) is?(false), false, true))) |
The Biild Object, revisited Code doesn’t need to be inside of a
is equivalent to:
|
|
Adding a task is as simple as calling
They can also be strings:
If you want to get fancy, i.e., mocking command-line arguments with tasks, names can also be lists which will alias the task to each name in the list.
Ioke also supports arguments with default values. The |
Biild addTask = method(name, body, desc nil, |
Both the task name and dependencies are taken from the name parameter.
If
The Since we can check if the |
deps = if(name kind?("Pair"), |
The dependencies are always treated as a list. If the dependency given is already a list it is left alone, otherwise its converted to a list. |
if(name second kind?("List"), name second, [name second]), |
If no dependencies were given in |
[]) |
The name of the task is also treated internally as a list, each element of the list being an alias for the task. |
if(name kind?("Pair"), name = name first)
names = if(name kind?("List"), name, [name])
names each(name, |
The fully qualified name of a task is dependent on its namespace.
The current namespace is stored in |
name = (ns + [name]) join(":") |
No two tasks can have the same name in the same namespace. |
if(tasks map(name asText) include?(name asText),
error!("Task `#{name}' already defined")) |
Since everything has worked out so far, a new |
@tasks << Task with(name: name, dependencies: deps, body: body, desc: desc))) |
Biild only recognizes certain files as valid Biildfiles.
The |
Biild getFiles = method(search_path,
["Biildfile", "biildfile", "Biildfile.ik", "biildfile.ik"] map(f,
search_path + FileSystem separator + f) select(f,
FileSystem file?(f))) |
The Biildfile DSL |
|
The DSL used in Biildfiles are implemented as destructuring macros. A dmacro is different from a regular macro in that is can destructure the arguments it is given and respond to them accordingly. Accepted signatures in dmacros are defined by argument lists. If an argument
is prefixed with |
namespace = dmacro( |
The |
[>name, code] |
A new namespace nesting level is added. |
Biild ns << name |
Then the code is evaluated in the context of whatever called the
|
code evaluateOn(call ground) |
Once |
Biild ns pop!) |
The |
task = dmacro( |
If only a name is given, a new task is created with
|
[>name] Biild addTask(name, true), |
The most common scenario is that both a name and body are given to a task.
|
[>name, code] Biild addTask(name, code), |
Tasks can also be given a description. If a description is given, it must be the second argument, and the body of the task is the third. |
[>name, >description, code] Biild addTask(name, code, description)) |
The Interface |
|
The
Or Ruby:
|
System ifMain( |
The first Biildfile that exists in the current working directory is used as the Biildfile. If one doesn’t exist, a condition is rained and processing stops. |
biildfiles = Biild getFiles(System currentWorkingDirectory)
case(biildfiles empty?,
false, source = FileSystem readFully(biildfiles first),
true, error!("No biildfile found"))
Message doText(source) |
If no command-line parameters are passed, then it is assumed that the
task named |
tasks = if(System programArguments empty?, ["default"], System programArguments) |
Finally, one by one, each task given on the command line is invoked. |
tasks each(taskName,
atask = Biild tasks select(t, t name asText == taskName) first
if(atask nil?, error!("No task `#{taskName}'"), atask invoke)
if(atask success not, error!("Task `#{atask first name}' failed")))) |
NotesThis annotation was generated by my fork of Rocco, the quick-and-dirty literate programming annotation generator. |
|