Latest News >> 2008-07-20 2008-06-25

I’ve been completely fed up with news/feed/rss/atom readers these days. I use Linux as my primary operating system, and I only have a few feeds that I want to rip through quick so I can get to reading the content. Yet, trying to find a reader that doesn’t suck donkey balls has been a chore.

2008-06-21

Wanna know what all the Ruby vulnerabilities are? Or at least have a fun look at how to search through code for clues? It’s a blast.

2008-06-13

I’m dropping a large blog post on everyone to just say that I haven’t died, I’ve just been busy working on my book for A/W about Mongrel. I had contracted with them to do a book about deploying Mongrel, but then decided it wouldn’t be a very good book since we’d already done one about that topic and there wasn’t too much more to say.

Vellum: A Simple Python Build Tool

NOTE: I'm working on a medium sized book about Vellum that will serve as
the primary documentation and also try to teach you about the art of
automating things.  You can grab the unfinished PDF
to follow my work and all the source to the book is in the Bazaar repository.

Vellum is my project to create a nice build tool for Python, or really for anything you need to automate in a consistent fashion. Vellum uses a simple format that takes no time to understand, but also lets you easily write Python modules that it can load so you can extend what Vellum does. Vellum tries to be safe whenever it can, and avoids complexity at all costs without losing features.

The primary goal with Vellum is to be simple, safe, and explicit. No more magic chains of things happening that you can’t figure out.

Features

  • You can write modules in Python and put them in your ~/.vellum/modules directory to get your own commands in Python when Vellum’s spec format isn’t capable enough.
  • Builds are written in a safe and very simple format that is like “Python minus the crap”.
  • Build files are not loaded with the Python interpreter and are actually parsed so they will (hopefully) be safer and easier to analyze.
  • Options, imports, dependencies, and targets are separated into different sections to make the file simpler to debug and write.
  • Call other targets out of order in those cases where the dependency graph has to be modified.
  • Vellum has tons of debugging output and prints very good meaningful error messages when it can.
  • Runs multiple or single lines of shell or Python code in a reliable way according to dependencies.
  • Output formatted messages to the user.
  • Test for true conditions to avoid running a target, which is more flexible than having target files that must match mtimes.
  • A simple code generator system that will let you create templates for generating tasks of particular types.
  • In addition to importing Python modules to make new commands, you can also import other build files to create master/child or more modular build systems.
  • Imported build specs are automatically put into a namespace so that names don’t clash, and you can rename the imported namespace to anything you like.
  • Since commands are functions, they become self-documenting in Vellum so that it’s easy to see what commands a build spec has as well as how to use it.

Download

You can get vellum either by using easy_install, from Launchpad, or through Bazaar.

From Launchpad

You can grab the latest version from the Launchpad project page for the 0.0 series. There should be .tar.gz files, .egg packages, ChangeLogs, and sometimes samples.

With easy_install

Vellum is now registered in the Cheese Shop so you can use easy_install to install it with one command:

$ sudo easy_install zapps pygments idiopidae vellum

Once you do this it should be available. All the .vel files should also be included in the distribution so you can check it out to see some sample. They’re also documented below.

You can also just install zapps and vellum if you don’t plan on building the PDF book but it might be good to get all the gear to help me test it.

Tracking The Source

Your best way to track what I’m doing is to grab the Bazaar repo:

bzr branch http://www.zedshaw.com/repository/vellum/

Let me know if that don’t work for some reason. Feel free to send me patches, and it’s GPLv3 licensed for now.

Getting Started

Vellum takes a build.vel file, loads it, and runs any targets you give it on the command line. Here’s the command line help:

Usage: vellum [options]

Options:
  -h, --help            show this help message and exit
  -f FILENAME, --file=FILENAME
                        Build file to read the build recipe from (no .vel)
  -q, --quiet           Tell Vellum to shut up.
  -d, --dry-run         Dry run, printing what would happen
  -k, --keep-going      Don't stop, build no matter what
  -T, --targets         Display the list of targets and what they depend on
  -F, --force           Force all given conditions true so everything runs
  -D, --dump            Dump the build out to a fully coagulated build.
  -s, --shell           Run the vellum shell prompt.
  -I, --install         Create the ~/.vellum directories.
  -v, --version         Print the version/build number.
  -C, --commands        List all commands and their help. Give a name to see just
                        that one.

As of 0.13-rev79 this is the options, but normally you don’t use these. If your build.vel had a target called build you’d do this:

$ vellum build

You can list as many targets as you like, and if the options stanza (mentioned below) has a default setting then that is run if you don’t give any targets.

Vellum’s Build Spec Format

Vellum uses a simple format for it’s build file, and then modules you can write in Python to implement custom commands. There is an overall philosophy to this design that comes from years of dealing with builds written by other people:

  1. The format goes along with Lisp’s idea that code is data and creates an executable description using simple lists, dicts, and name(value) expressions. You can think of Vellum’s format as Lisp+dicts where every “function” takes only one argument (which can be a list, or dict, or function that …)
  2. The goal of the main build.vel and any recipes is to make something that is easy to analyze (both by a person and by Vellum) without executing any code. This is important for safe software distribution and distributed builds.
  3. In order to provide flexiblity, Vellum let’s you create commands with Python but loads them from trusted places.
  4. Everything about the build format and the command modules is designed to let you create reusable builds. This is why the dependencies are separated from the targets. With build systems that attach the dependencies to the targets it’s too hard to just reuse the targets in other builds. Breaking the dependencies out means that you can pull in whole builds as components and reuse them, even with name spaces. It’s also easier at a glance to see what every task does and what it will call.
  5. Finally, unlike many other tools, Vellum does not force you to put all of the processing logic into an entirely declarative structure. Some tasks are better specifie in an imperative way, so Vellum allows you to specify dependencies at any point in a target’s definition.

Here’s the main Vellum build.vel as your first sample:

# This is a working build spec, but it is also an example
# so it consists of a lot more stuff than you would normally
# find in a real project.

imports [
    recipe(from 'scripts/testing' as 'testing')
    recipe(from 'scripts/dist' as 'dist')
    recipe(from 'scripts/sample' as 'sample')
    recipe(from 'doc/book' as 'book')
]

options(
        project "vellum"
        default 'tests'
        sudo 'sudo'
        version '0.17'
        website '../zedshaw.com/output/projects/vellum'
        bzr.revision '.bzr/branch/last-revision'
        version.file 'vellum/version.py'

        setup(
            name 'vellum'
            version '0.17'
            author 'Zed A. Shaw'
            description 'A flexible small make alternative for Python programmers.'
            author_email 'zedshaw@zedshaw.com'
            url 'http://www.zedshaw.com/projects/vellum'
            packages ['vellum']
            scripts ['bin/vellum']
        )
)

depends(
        build ['tests' 'version.gen' 'dist.install' 'dist.sdist']
        commit ['dist.gen.setup' 'parser' 'dist.clean' 'book.clean']
        tests ['parser' 'testing.run']
        release ['build' 'dist.release' 'book.release' ]
)

targets(
        commit [
            $ bzr log --short > CHANGES
            $ bzr commit
            $ bzr push
        ]

        version.gen [
            py [
                |rev = open("%(bzr.revision)s").read().split()
                |ver = {"version": version, "rev": rev}
                |open("%(version.file)s", 'w').write(
                |    "VERSION=" + repr(ver))
            ]
        ]

        parser "zapps vellum/parser.g"

        dist $ cp doc/manual-final.pdf %(website)s

        cloc [
           $ cloc --report-file=doc/test_cloc.txt --no3 --by-file tests
           $ cloc --report-file=doc/source_cloc.txt --no3 --by-file --force-lang=python,g vellum bin
           $ cat doc/*_cloc.txt
        ]
)

Here you see that you get Python’s usual comment style, then you have a format that consists of what looks like function calls with weird parameters in them and lists.

The file breaks down into a logical structure like this:

  • imports—Other .vel files to load as “recipes” using import commands or “modules” that have Python definitions of new commands you want.
  • options—A dict sets various options the rest of the file can use in commands as % replacements.
  • depends—A simple dict that says which targets (or fake targets) need which other targets first.
  • targets—A dict with the targets and what to run for each one as commands and shell.

The format is then very simple once you understand a few little grammar rules:

  1. Each element is a reference which consists of a name and an expression. Example: targets(commit “echo test”) is a reference that has the name “targets” and has a dict as the expression (which also contains a reference named “commit” that has an expression “echo test”).
  2. An expression can be anything else, even another reference. You get NUMBERs, STRINGs, lists, dicts (but with parenthesis), and a special thing called a “line string”.
  3. During processing Vellum assumes that a plain string is just a shell command. Since this is so common there’s a special string that works like Python’s comments called a “line string”. Any line that starts with $,>, or | is turned into a string preserving all space. This lets you easily do multi-line Python or shell commands and makes the file look nicer.
  4. The whole file is a dict, which means that order doesn’t matter for the above mentioned stanzas.

This means that you do one reference for each of the above stanzas, and then you put the required data in each using the rules above.

Grammar (For Experts)

The grammar for Vellum is written for Zapps and is actually incredibly simple considering it can encode quite an extensive data layout. If you take a look at this:

rule input: ( reference | COMMENT LINE)* ENDMARKER 
rule reference: NAME expr 
rule expr: atom | reference | structure 
rule atom: NUMBER  | STRING | SH LINE 
rule structure: 
    LSQB elements? RSQB  
    | LPAR dictmaker? RPAR 
rule elements: ( expr )+ 
rule dictmaker: ( reference )+ 

You should be able to figure out how things are structured. Start at the top with input (that’s the whole file) then trace down through reference, expr, atom, structure, elements, and dictmaker. You’ll notice this is very similar to Python’s Grammar file and that’s on purpose. The original Vellum format was just that, then I stripped out all the elements which weren’t needed to encode Vellum’s build.

That’s why Vellum’s build format doesn’t need any commas, braces, semicolons, colons, etc. Just as close to pure data as I could get things.

The interesting thing is that, when a reference is inside a list it’s simply put in a list form like you’d expect, but when it’s placed inside a dict (using ( ... ) format) the reference.name and reference.expression is converted into a dict’s key=value pair. What this does is give you a simple consistent way to build lists of anything or dicts of anything or do commands that look like this:

gen(from "infile.txt" to "outfile.txt")

This looks like a function call, but really it’s just a reference named “gen” that has a dict which in Python is: {“from”: “infile.txt”s, “to”: “outfile.txt”}

Building Our First

With the structure in your head we can build a small vellum build to say “Hello World” in different ways:

# Say that the hello target is the default.
options(
    default 'hello'
)

# Load the vellum commands (mandatory for now)
imports [
    module(from 'vellum.commands')
]

depends(
        hello [ 'py.hello' 'sh.hello' 'line.hello' ]
)

targets(
        py.hello py "print 'hello world'"

        sh.hello 'echo hello world'

        line.hello [
            $ echo 'hello world with $'
            | echo 'hello world with |'
            > echo 'hello world with >'
        ]
)

This is a really elaborate hello world demo, but it shows you all the main things you need:

  1. How to set a default target.
  2. How to import the vellum.commands so you can actually do stuff (this will change and just be a default later).
  3. How to specify the depends (just a reference with a list of strings).
  4. What targets can look like, including how they can just be a single command reference, a string, an array of line strings.
  5. What the different line strings look like with $, >, and | chars. These are handy, and I tend to use $ for shell commands and | for Python code that needs to be multi-line. You can also pass another list of line strings to the sh and py commands to do multi-line targets.

When we run this we get:

BUILDING: ['py.hello', 'sh.hello', 'line.hello', 'hello']
-->: py.hello
py: print 'hello world'
hello world
-->: sh.hello
sh: echo hello world
hello world
-->: line.hello
sh: echo 'hello world with $'
hello world with $
sh: echo 'hello world with |'
hello world with |
sh: echo 'hello world with >'
hello world with >
-->: hello

Which shows you Vellum transitioning into each target, printing what the command is, then actually doing it.

Available Commands

Vellum build specs have a decent set of commands already, and you can write your own (documented further down).

As of version 0.13-rev79 these are the commands and their full documentation which you can get easily by running vellum -C and you can give just a single command to get that documentation:

given:
    Evaluates the list of strings or string as a Python
    expression (not statement) and if that expression is
    False stops processing this target.  Read it as:
      given X is True continue.

    Usage: given 'os.path.exists("/etc/passwd")'
    
unless:
    The inverse of given, this stops processing
    if the expression is True.  Reads as:
      unless X is False continue.

    Usage: unless 'not os.path.exists("/etc/passwd")'
    
log:
    Logs the string to the user.

    Usage: log "hi there user"
    
py:
    Runs the given list of strings or string as a python
    statements.  It doesn't stop, but if you raise an
    exception this will obviously stop the processing.
    You have access to all the options as globals.

    Usage: py 'print "hi"'
    
needs:
    Indicates that before this target should continue,
    vellum needs to run the targets in the given list.
    If dependencies for a target don't fit in the main
    listing then use needs to put them in the target.

    Usage: needs ['clean', 'build', 'dist']
    
mkdirs:
    Takes a dict with "paths" and "mode" and then creates all of those
    paths each with the given mode.  It will also expand user paths
    on each one so you can use the ~ shortcut.

    Usage:  mkdirs(paths ["path1","path2"] mode 0700)
    
sh:
    Runs the given list of strings or string as a shell
    command, aborting if the command exits with 0.

    Usage: sh 'echo "test"'
    
install:
    Simple command that installs Vellum's ~/.vellum 
    directory for you.  You can also just use: vellum -I.
    WARNING: This might get replaced with something more
    like the install command.

    Usage: install
    
forall:
    Iterates the commands in a do block over all the files
    matching a given regex recursively.  You can put anything
    you'd put in a normal target in the do block to be executed,
    and when it is executed the 'as' variable is set to the
    full path of each file.  This will also be in each task
    you transition to with the 'needs' command.

    Usage: forall(files ".*.py$" as "file" do [ ... ])
    
gen:
    Used to do simple code generation tasks.  It
    expects a "from" and "to" argument in a dict
    then it loads from, string interpolates the
    whole thing against the expr merged with the
    options, and finally writes the results to "to".

    Usage:  gen("from" somefile.txt "to" outfile.txt also "this")

Writing Your Own Commands

Vellum makes it easy for you to extend and modularize the build in two ways:

  1. Place alternative or common .vel files in ~/.vellum/ and then import them in the imports stanza.
  2. Write Python code as functions which become commands and place it in either the regular Python path or ~/.vellum/modules.

If you want to write a Python module to give it commands, you just have to follow two rules:

  1. The function takes a vellum.Scribe object and the expression that was passed in the build spec for you to process.
  2. Oddly, you return True if you want processing of the current target to stop. This is so you can just write the function and do nothing to have Vellum keep going (since the default Python return is None).

Once you have that down you’ll just need to look at the following examples of the main commands and read the documentation to vellum.Scribe.

from __future__ import with_statement
# Copyright (C) 2008 Zed A. Shaw.  Licensed under the terms of the GPLv3.

import os
import sys
import fnmatch
import subprocess

def sh(scribe, expr):
    """
    Runs the given list of strings or string as a shell
    command, aborting if the command exits with 0.

    Usage: sh 'echo "test"'
    """
    formatted = scribe.interpolate("sh", "".join(expr))
    scribe.log(" sh: %r" % formatted)
    if not scribe.option("dry_run"):
        retcode = subprocess.call(formatted, 
                shell=True, stderr=1, stdout=1)
                
        if retcode != 0: scribe.die(expr)

def py(scribe, expr):
    """
    Runs the given list of strings or string as a python
    statements.  It doesn't stop, but if you raise an
    exception this will obviously stop the processing.
    You have access to all the options as globals.

    Usage: py 'print "hi"'
    """
    formatted = scribe.interpolate("py", "".join(expr))
    scribe.log(" py: %r" % formatted)
    if not scribe.option("dry_run"):
        scribe.push_scope({"scribe": scribe, "script": scribe.script})
        exec(formatted, globals(), scribe.options)
        scribe.pop_scope()

def log(scribe, expr):
    """
    Logs the string to the user.

    Usage: log "hi there user"
    """
    scribe.log(" " + scribe.interpolate("log", expr))

def needs(scribe, expr):
    """
    Indicates that before this target should continue,
    vellum needs to run the targets in the given list.
    If dependencies for a target don't fit in the main
    listing then use needs to put them in the target.

    Usage: needs ['clean', 'build', 'dist']
    """
    for target in expr:
        if not scribe.is_target(target):
            scribe.die(target, "target %s isn't in the targets list" % target)
        else:
            scribe.transition(target)

def given(scribe, expr, name='given'):
    """
    Evaluates the list of strings or string as a Python
    expression (not statement) and if that expression is
    False stops processing this target.  Read it as:
      given X is True continue.

    Usage: given 'os.path.exists("/etc/passwd")'
    """
    formatted = scribe.interpolate("given", "".join(expr))
    scribe.log(" %s: %r" % (name, formatted))

    if scribe.option("force"): return False
    try:
        return not eval(formatted)
    except Exception, err:
        scribe.die(expr, err)

def unless(scribe, expr):
    """
    The inverse of given, this stops processing
    if the expression is True.  Reads as:
      unless X is False continue.

    Usage: unless 'not os.path.exists("/etc/passwd")'
    """
    if scribe.option("force"): return False
    return not given(scribe, expr, "unless")

def gen(scribe, input=None, output=None, **expr):
    """
    Used to do simple code generation tasks.  It
    expects a "from" and "to" argument in a dict
    then it loads from, string interpolates the
    whole thing against the expr merged with the
    options, and finally writes the results to "to".

    Usage:  gen(input somefile.txt output outfile.txt other "variable")
    """
    scribe.log("gen: input %s output %s" % (input, output) )
    if not (input or output): 
        scribe.die("gen", "You must give both input and output for gen.")

    if scribe.option("dry_run"): return
    expr.update(scribe.options)
    with open(input) as inp:
        with open(output,'w') as out:
            out.write(inp.read() % expr)

def install(scribe, expr):
    """
    Simple command that installs Vellum's ~/.vellum 
    directory for you.  You can also just use: vellum -I.
    WARNING: This might get replaced with something more
    like the install command.

    Usage: install
    """
    # relies on mkdirs to honor dry_run
    mkdirs(scribe, 
           paths=["~/.vellum/modules", "~/.vellum/recipes"], 
           mode=0700)

def mkdirs(scribe, paths=[], mode=0700):
    """
    Takes a dict with "paths" and "mode" and then creates all of those
    paths each with the given mode.  It will also expand user paths
    on each one so you can use the ~ shortcut.

    Usage:  mkdirs(paths ["path1","path2"] mode 0700)
    """
    assert isinstance(paths, list), "mkdirs expects a list as the expression"

    for dir in (os.path.expanduser(p) for p in paths):
        scribe.log(" mkdir: %s" % dir)
        if not (os.path.exists(dir) or scribe.option("dry_run")):
            os.makedirs(dir, mode=0700)

Getting More Help

Since Vellum is very new, feel free to email me (mentioned in the footer) and I’ll help you out.

Also assume that Vellum has bugs, but that it should work for most tasks. Feel free to post any bug reports to the Launchpad project

License

Vellum is licensed under the GPLv3 but since it is a build tool that you normally don’t distribute with your software it shouldn’t impact you. In general the GPL only applies if you write something that links against Vellum and distribute that something. In fact you could use a Vellum build spec and build your software with Vellum and keep your software commercial or use any other license.

This is much the same way that GCC and GNU make works. Just using them does not mean that your work is instantly GPLed. In more direct terms, the GPLv3 does not apply to any build specs (.vel) files you write since those are parsed, and does not apply to anything you build with Vellum (since building is not linking).

Now, if you write modules to load into vellum then those are linked into Vellum. You’ve got a couple options:

  1. Since it’s a build system, just don’t release your Vellum build to customers. This is the norm when you distribute software anyway, so it should be fine.
  2. If you have to give them Vellum and modules you wrote so they can build it, then turn your build module(s) into a GPLv3 project and release then. You don’t have to release all of your softare, just the build modules you wrote. If you’re smart, you’ll make the build modules generic enough that it won’t matter if you release them.
  3. Vellum is a build tool, so if there’s any proprietary build modules, just convert them to a script and run them as a shell command.

The Distinction

When would you be required to GPLv3 your software? In the above scenarios you are safe to use Vellum for nearly any task assuming you release your modules or never release your build. When you have to GPLv3 your software is if your software directly imports Vellum in order to operate. Don’t think you can get around this, since, if your software does an “import vellum.*” it now has to be GPLv3.

The distinction is when Vellum is being used in your software lifecycle. Using it to build means no GPLv3 (except for your modules). Using it to give your customers functionality (meaning, after the build) means you have to GPLv3 your stuff.