A straightforward and versatile build system written in python
JavaScript Python CSS Other
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


<!!!> New puke ahead! Read https://github.com/webitup/puke/wiki <!!!>

Building: puke it right

  / `. ,' \
 |  ,' `.  |
 |   ___   |
  \ ( . ) /


This is mostly an experimental tool, built out of a growing displeasure having to work with existing similar softwares, and for the sole purpose of fitting our specific build needs.

While giving puke an OSS license made sense, while the author is happy to work on it, and while it might suit you, it's quite likely that it won't, and that you would find yourself more satisfied with one of the other solution out there (rake and scons come to mind).


We are using python, and a homemade system named "puke". It's quite similar to rake, jake, jasy, scons, etc, except:

  • it doesn't suck ass, unlike ruby
  • it installs painless, unlike ruby
  • it's extremely straightforward and does just a couple of simple things, avoiding both bloat and indigest documentation
  • it's python, so it's cool and sexy, unlike ruby :)


Basic file manipulation, js linting via closure, minification via closure and YUI, scss parser, js documentation via jsdoctoolkit, VirtualEnv creation with package installation, system package check.



  • Speak mode on build fail/success (Mac OS X only)
  • console.say on Mac OS X (e.g console.say("Build failed!"))
  • Require.merge() makes now a deep merge
  • sh() can take now multiple commandes : sh(['cd somepath', 'do something'])
  • prompt('message', 'default') to prompt during the build
  • New FileSystem api
  • New System api
  • New Utils api
  • New VirtualEnv api
  • patch('dir/to/patch', patchfile) (Supports unix patch format only : man patch)
  • Task parameters (puke task arg1 arg2)
  • Task infos (puke task -i docstring style)
  • few fixes


There is two ways to get there.

Either the recommended sandboxed way (using brew, for MacOSX), read 1.

Or the "system" way (MacOSX and Linux), read 2.

If you don't understand what I'm saying, you are on mac, so just follow 1. If you do, then you already have a working python + easy_install environment, right? Move on to step 3.

1. Sandboxed way

# Install XCode:

# Install brew:
sudo mkdir /usr/local
sudo chmod g+rwx /usr/local
sudo chown root:staff local
/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/gist/323731)"

# Install python with brew:
brew install python

# Update your .profile so that the brew python is used
echo 'export PATH="/usr/local/share/python:/usr/local/bin:/usr/local/sbin:$PATH"' >> ~/.profile
source ~/.profile

# Double-check that the sandboxed python is used
which easy_install
# Should output/usr/local/share/python/easy_install
which python
# Should output /usr/local/bin/python

Now go to step three.

2. The system way

You have nothing special to do, but you will need to be root in some of the following steps.

Go to step three now.

3. Install pip

If on Mac:

easy_install pip

If on Linux, just make sure you got pip:

# If debian based...
sudo aptitude install python-pip
# or do it whatever way pleases you

4. Install puke

# Get some puke on you
pip install --upgrade puke

Whenever you want to upgrade to the latest version, just do it again

pip install puke --upgrade

Yeah, that's it.


  • some puke functionalities require a working jre. Lion has one by default (does it?). Ubuntu fans might just do a:
sudo aptitude install openjdk-7-jre
  • if you happen to have libyaml-dev installed, you need to have python-dev as well. This is not a puke requirement, but a pyyaml gotcha actually.

Oh, did I mentioned the BIG FAT WARNING MANU BUG?

First time puke runs, it does patch some internal dependency (closure). So, IN THE CASE YOU USED THE SYSTEM WAY, do this after install:

sudo puke --patch

Don't forget to do that (yet again, only if you installed puke as ROOT) every time you install.

That's it.


You pretty much create a puke file, which is a very simple python script that describes build tasks for your project. You may name it either "pukefile", or "pukefile.py".

Then you just call "puke someTaskName".

Help me puke :

$ puke --help
Usage: puke [options]

  -h, --help            show this help message and exit
  -c, --clear           Spring time, clean all the vomit
  -q, --quiet           don't print status messages to stdout
  -v, --verbose         print more detailed status messages to stdout
  -t, --tasks           list tasks
                        Write debug messages to given logfile
  -f FILE, --file=FILE  Use the given build script
  -p, --patch           Patch closure
  -s, --speak           puke speaks on fail/success (Mac OS X only)

Can't remember your tasks ? Just puke it

$ puke --tasks
 No tasks to execute. Please choose from: 
 test: coin coin
 simple: Simple Test
 lint: lint
 doc: Documentation
 gate: Build gate package

Puke 10 seconds API reference


#!/usr/bin/env puke
# -*- coding: utf8 -*-

Defining a simple task:

Name your task "default" in order to have it executed by simply puke-ing

@task("Simple Test")
def simple():
   console.log("Do something")

import python script

| pukefile.py
| helpers.py

=> helpers.py
from puke import *

def test():
  f = FileList('some/path')
  print "success"

=> pukefile.py
import helpers


Calling a task from another task:


Defining "default" task:

def default():

@task("Simple Test")
def simple():
   console.log("Do something")

Executing tasks with args

Your pukefile

@task('with args')
def test(required, optional = 'default'):
   """Python docstring style comment"""
   print "Required : %s" % required
   print "Optional : %s" % optional

Execute puke

puke test value
>>> Required : value
>>> Optional : default

Need help with your params ?

puke test --info
>>> -------------------------------------
>>> * Help test (task description) 
>>> -------------------------------------
>>> Help on function test in module puke:
>>> test(required, optional = 'default')
>>>    Python docstring style comment

Require (json / yaml)

r = Require('global.yaml')

Working with environment variables (Yaml example):

  #           envvar name | default
  build_dir: "${BUILD_DIR}|/usr/toto/build/"
  string: "toto"

Yak it and make the puke easier!

r = Require('global.yaml')

@task("Simple Test")
def simple():
   console.log("Easy to get my conf", Yak.build_dir, Yak.string)

#Check if your param exists
if 'build_dir' in Yak:
   print "yes"

Straight access to environment variables

Env.get('BUILD_DIR', 'default')


# Info levels
console.info("info", arg2, ...)
console.confirm("confirm", arg2, ...)
console.log("log", arg2, ...)
console.debug("debug", arg2, ...)
console.warn("warn", arg2, ...)
console.error("error", arg2, ...)


#Mac OS X only
console.say("Build failed!")

Speak options (Mac OS X only):

  #Manually enable/disable speak mode
  Console.SPEAK_ENABLED = True

  #Custom message on build fail
  Console.SPEAK_MESSAGE_ON_FAIL = "Shit happens..."
  Console.SPEAK_MESSAGE_ON_SUCCESS = "Holy macaroni!"

  #Deactivate success alert

  #Deactivate fail alert

Prompt :

answer = prompt('How are you doing ?', 'fuck off')

Getting a FileList:

list = FileList("foldername", filter = "*.js", exclude = "*.min.js")`

# multicriteria
list2 = FileList("src", filter = "*.css,*.scss")

#merge lists


list = ["somefilepath", "someother", "http://example.com/something"]

Note that http:// urls are supported

Note that SCSS are automatically parsed

Merging files into one:

combine(list, "build/test.js")

Deep-copying (folder list):

deepcopy(list, 'build/copy/')

Minifying (with closure for js, and yahoo ui for css):

minify("build/test.js", "build/test.min.js")

Patch (unix patch format)

patch('dir/to/patch', patchfile)

Pack/unpack (gz, zip)

Packing :

pack(list, "folder/something.zip")

pack(list, "folder/something.tar.gz")

Unpacking :

unpack('folder/something.zip', 'folder/test-unpack/')

unpack('build/something.tar.gz', 'folder/test-unpack/')

Call system:

#get output
pwd = sh("pwd")
#multiple commands
sh(['cd somepath', 'do something])

Get FileList stats:

stats(list, title = "JS Stats")
 - JS stats :
   ~ Files : 65
   ~ Lines : 23477  (361 per file)
   ~ Size : 1.8KB (28.0bytes per file)

Perform in-file pattern replacement:

sed = Sed()
sed.add('$TOTO$', 'troulute')
combine(list, "build/test.js", replace = sed)
deepcopy(list, "build/test.js", replace = sed)

Using jslint (see linting for more):

jslint(list, fix = False, relax = False, fail = True)

Using jsdoctoolkit (see documenting javascript for more):

jsdoc(list, "docdestination", [template = "templatepath"])

Clear puke cache

$ puke -c
 Spring time, cleaning all the vomit around ...
 You're good to go !


Creates missing hierarchy levels for given directory


Get file content


Remove file or dir (recursively)

#With protection if you're trying to remove : './', '/', '~/', '~', '.', '..', '../'

Copy a file

#creates dirs if dst doesn't exist
#Does the copy only if the file doesn't exist or if the file has been modified
#To force to copy anyway, use  force = True
FileSystem.copyfile(src, dst)

Create a file and write your content

FileSystem.writefile('file', 'content')

Check if the path exists


Check if the path is a file


Check if the path is a dir


Get an OS path (eg : build/lib/folder on Unix)

FileSystem.join('build', 'lib','folder')

Get the absolute path


Get file name

>>> 'toto.py'


Check platform

if System.OS == System.MACOS:
   #do something macos related
elif System.OS == System.LINUX:
   #do something linux related
elif System.OS == System.WINDOWS:
   #Achtung windows ...

Get user login

#Return None if something went wrong

Check if a system package is here :

>>>   nginx is M.I.A
>>>     => "brew install nginx"
>>>   /!\ BUILD FAIL : nginx not installed

#Check version too (>= <= > < ==)
System.check_package('varnish', '>=3.0.1')
>>> varnish : INSTALLED (3.0.0 not >=3.0.1)
>>> * Continue anyway ? [Y/N default=Y]
>>> N
>>>  /!\ BUILD FAIL : Failed on version comp varnish 

#Only check on specific platform (System.LINUX, System.MACOS, System.LINUX, "all")
System.check_package('libcaca', platform=System.LINUX)

Get package version

>>> "0.9.9"

VirtualEnv (Create and manage virtualenvs)


env = VirtualEnv()
env.create('path/to/create/env', python='python3|python|python2.7|...')
>>> * Creating env "test" ...
>>>   virtualenv : OK (1.6.4)
>>>   Python version : 3.2.2 ...
>>>   Env "test"  created 

Load an existing virtualenv

env = VirtualEnv()

install package

>>> * Install "webob" in env "test" ...
>>>   Package "webob" is ready

install package in a specific version

env.install('webob', '1.1.1')
>>> * Install "webob" in env "test" ...
>>>   Package "webob" is ready

install / force upgrade if installed

env.install('webob', upgrade=True)

upgrade package / all packages


Check package / auto fix

#check if the named package respect the version
env.check_package('webob', '>=1.1.1')

#check it and fix if the cond fails (make an install or upgrade or downgrade)
env.check_package('webob', '>=1.1.1', fix = True)

List packages

>>>  * List packages (env "test")
>>>    - PIL (1.1.6)
>>>    - PyYAML (3.10)
>>>    - WebOb (1.2b2)

Package info


Remove env



Merge two deep dicts non-destructively

a = {'a': 1, 'b': {1: 1, 2: 2}, 'd': 6}
b = {'c': 3, 'b': {2: 7}, 'd': {'z': [1, 2, 3]}}
Utils.deepmerge(a, b)
>>> {'a': 1, 'b': {1: 1, 2: 7}, 'c': 3, 'd': {'z': [1, 2, 3]}}


MIT license, see LICENSE file




Don't puke on yourself!

Bitdeli Badge