bashup / .devkit

Portable project automation and dependency bootstrapping for polyglot projects
MIT License
23 stars 2 forks source link
bash dependency-manager devops project-automation scripts-to-rule-them-all

.devkit: development automation & dependency management

If you're working on a project that:

your choices are kind of limited. You can use a Makefile, maybe, and what... git submodules? Vendoring with git subtrees? One of the many bash package managers that don't really get along with each other, and which your collaborators would have to install on their machines? An entire Vagrant VM or collection of docker images?

Sure, you can solve the standardization part of the problem with a Scripts to Rule Them All-style script/ directory, but those are kind of a pain to make and not terribly reusable from one project to the next.

.devkit solves these problems by giving you an extensible automation and development-dependency management framework that you don't have to bundle in your project.

Instead, your project's script/ directory contains a short bootstrap that fetches .devkit, and all of the other script/ files are just symlinks to bootstrap.

Your project then defines any custom commands and variables in a .dkrc file, and gets to use all the tools and modules available in .devkit, inlcuding a local version of basher for git-based dependency fetching.

Dependencies are installed to a .deps directory, with executables in .deps/bin -- which is added to PATH while your commands run. You can also add new script/ types of your own, or just run the extra commands with .devkit/dk commandname.

Contents

Project Status

At this moment, devkit is still very much in its infancy, and should be considered alpha stability (i.e., expect rapid and likely-breaking changes). This is also all the documentation there is so far, and there are many modules planned but yet to be added. For right now, you'll also need to read the dk source and loco docs to see the full API available to you (beyond what's listed here).

Installation

To use this in your project, just do the following in your project root:

$ git clone -q https://github.com/bashup/.devkit
$ .devkit/setup
devkit setup is complete; you can now commit the changes
$ git commit -m "Added devkit"

The .devkit/setup command will create and git add a script/ directory for you, with unchanging bootstrap code. It also adds .deps and .devkit to your .gitignore, and creates a default .envrc and .dkrc if you don't already have them.

In addition to installing the project tools locally, you can install a global copy of the dk binary as a way to run commands that don't have script/ counterparts, without needing to explicitly type .devkit/dk to run the local copy. (A global dk will exec the local copy of dk if one is present and not the same file as the global dk.) If you use basher, you can basher install bashup/.devkit to do the global install.

Configuration and Extension

The .envrc is a bash file that configures PATH, MANPATH, and various basher-specific variables to install dependencies under .deps, with commands and manpages directed to .deps/bin and .deps/man. (It also adds any project-local composer or node.js local tools to the PATH.) It's automatically sourced when running script/ commands or a .devkit/dk subcommand.

(As a convenience feature, if you or your users have direnv installed and running, you or they can direnv allow the .envrc so that it's automatically sourced whenever you enter the project directory, and reset when you leave, giving you easy access to all your locally installed tools. Alternately, you or they can manually source it in a subshell.)

The .dkrc file, on the other hand, is a bash file where you can define your own commands or override existing ones, as well as activating any .devkit modules you want to use. For example, this .dkrc defines a test command that uses bats:

dk.test() {
    # check if we have `bats` command, if not, install it locally
    require bats basher install sstephenson/bats
    # Run tests
    bats tests/*.bats
}

When somebody checks out the project and runs script/test the first time, the following things will happen:

On subsequent runs of script/test (or .devkit/dk test), none of the cloning takes place, since the needed things are already installed.

(Note: you can, if you wish, vendor .devkit within your project or use a submodule so your users don't end up cloning their own copy, but if you're trying to pin a specific version it's probably easier to just edit your script/bootstrap to fetch the exact .devkit version you want from github.)

Automatic Dependency Fetching

.devkit extends the package.sh format with support for fetching development dependencies from github. If you create a package.sh containing a BUILD_DEPS variable, e.g.:

BUILD_DEPS=some/package@some-tag-or-branch:other/package

Then when any script/ commands (or the dk command) are run, the github packages some/package (at some-tag-or-branch and other/package (at master) are cloned to .deps/some/package and .deps/other/package if those directories don't yet exist. If the cloned packages have BINS listed in their package.sh, then those files are symliked into .deps/bin.

Note: for dependency fetching to work correctly, both the project's BUILD_DEPS and its dependencies BINS variables in their respective package.sh files must be written without any quote marks, escapes, environment variables, etc., as .devkit reads them exactly as written; e.g. BINS="foo:bar" will be read as two executables named "foo and bar" (i.e., with the quotes included).

.devkit Modules

Currently, .devkit provides the following modules you can dk use: in your .dkrc:

For any project:

For projects using Python:

For projects using PHP:

For projects using bash 3.2

You can activate any of them by adding "dk use: modules..." to your .dkrc, then defining any needed variable or function overrides. (Typically, you override variables by defining them before the dk use: line(s), and functions by defining them after.)

Note that these modules are not specially privileged in any way: you are not required to use them to obtain the specified functionality. They are simply defaults and examples. You can write your own modules and put them in a .devkit-modules subdirectory of your project root, and dk use: will look for modules there before searching .devkit's bundled modules.

External Modules

You can also load basically any file from github as a .devkit module, by specifying a module name of the form:

+ org / repo [ @ref ] [ :module-path ]

That is, doing e.g. dk use: +foo/bar@baz:spam will check out the baz branch or tag of foo/bar from github into your .deps directory (if there's not already a repo there), and then search for one of these files:

Both the @ref and :module-path parts are optional, defaulting to master and .devkit-module respectively, with dk use: +foo/bar checking out the master branch of foo/bar and searching for one of these files:

This means that projects that want to provide .devkit support can include a .devkit-modules/default or .devkit-module file, allowing others to use it with dk use: +some/project, automatically including the repo at build time, and adding its executables to .deps/bin. A project can also be created to just publish a bunch of .devkit-modules, or you can just literally source any file you like from any project on github by using an explicit module-path in the module name.

All-Purpose Modules

cram

The cram module defines a default dk.test function to provide a script/test command that automatically installs a local copy of the cram functional testing tool, and runs it on specs/*.cram.md files with 4-space indents, colorizing the diff results (if pygmentize is available) and piping the result through less.

To use this module (as .devkit itself does), use: it in your .dkrc, like so:

dk use: cram

You can then override the module's defaults by defining new functions.

For example, if you wanted to change the files to be processed by cram, you can redefine the cram.files function, and to change the pager, redefine the cram.pager function. To change the cram options, set the CRAM environment variable, or add a .cramrc file to your project.

As long as you run cram via script/test, dk test, or dk cram, you can place files named cram-setup.sh in your test directories, and they will be silently sourced at the start of each test file. Any functions or variables you define will then be available for your tests, and the setup file can access any cram environment variables (e.g. $TESTDIR and $TESTFILE). Setup files should not produce any output, or they will break the corresponding tests.

Setup files can source other shell files, including other directories' cram-setup.sh files if you need to share setup between directories (e.g. source "$TESTDIR/../cram-setup.sh" to source and extend a parent directory's setup). You can also just symlink from one cram-setup.sh to another.

shell-console

The shell-console module implements a dk.console function to provide a script/console command that starts a bash subshell with the devkit API and all variables available -- a bit like dropping into a debugger for the dk command. This is particularly handy if you don't have or use direnv, as it basically gives you an alternative to typing script/foo or .devkit/dk foo: within the subshell you can just dk foo.

To activate this in your project, add a dk use: shell-console line to your .dkrc, just like .devkit does. Running dk console or script/console will then enter a subshell.

Watch Modules

It's a common task to want to watch files and run commands when they change. devkit currently supports three file watching tools:

Each of these tools has different strengths and weaknesses. entr only runs a single command, and has to be piped a list of files (but can detect when the list needs to be refreshed). modd and reflex can run multiple commands based on matching rules, but have their own behavioral quirks.

reflex can handle regular expressions and can detect directories being created during a watch, but scans all files by default, which means you must explicitly add exclusions for anything your project builds, or it will cause infinite build loops. Its built-in globs do not support recursion or brace expansion, so devkit tries to emulate these features using bash brace expansion and converting globs to regular expressions.

modd, on the other hand, only processes globs, but has brace expansion and recursion built in. It doesn't detect new directories being created, but only matches explicitly-given patterns, so you're less likely to create an infinite loop by rerunning a build as the result of running a build.

entr-watch

The entr-watch module defines a default dk.watch command to provide a script/watch command that watches for file changes (using entr) and reruns a command (dk test by default). To enable it, dk use: entr-watch in your .dkrc, and then optionally define a watch.files function to output which files to watch. (By default, it outputs the current directory contents and any test.files.)

The watch command requires the entr and tput commands be installed. The former is used to watch files for changes, and the latter to compute how many lines of watched command output can be displayed without scrolling. (The watched command's output is cut off using head, and the screen is cleared whenever the watched command is re-run.)

modd-watch

The modd-watch module lets you define "watch rules": file patterns to watch for, combined with commands to run those when files change. Running dk watch (or script/watch if you create a link for it) will run modd to watch the files and run commands. (If modd isn't installed, the watch command will try to install it to the project .deps directory using go install.)

The way you add rules is by calling the watch or watch+ functions from your .dkrc. Each of these functions accepts zero or more glob patterns (optionally negated with a leading !), followed by -- and a command to run. If no globs are specified, the command will run once, at the beginning of the watch. (You can also add global exclusion globs with unwatch.) Some examples:

# When a .cram.md file under specs/ is changed, rerun tests; also run them at start of watch:
watch+ 'specs/**/*.cram.md' -- dk test

# The `watch+` command above is shorthand for these two commands:
watch -- dk test
watch 'specs/**/*.cram.md' -- dk test

# But in some cases, you will want the initial and on-change commands
# to be different.  For example, the below will run a full tree sync
# at start of watch, but sync only individual files when they change:
#
watch -- wp postmark tree posts/
watch 'posts/**/*.md' -- wp postmark sync @mods   # just sync modified files

# Exclude specified pattern(s) from ALL watches, past and future.
unwatch 'build/**' '**/.~*.md'  # ignore files under build/ and editor temp files

# Brace expansion can be used
watch '**/*{.sass,.scss}' -- dk build

Notice that glob patterns must be quoted to prevent the shell from interpreting them, rather than passing the wildcards to the watch command.

The difference between watch and watch+ is that watch+ runs the command immediately upon running dk watch, in addition to when files change. (This avoids the need to save something just to force the command to run.)

If you need the initially-run command to be different, you should separate your commands into watch -- init-command... and watch [glob...] -- change-command..., to run init-command when the watch begins, and change-command when changes occur.

Finally, note that if your .dkrc file is changed during a watch run, the dk watch command will re-execute itself to reload the changed watch configuration. If you have other files that should trigger such a configuration reload, you can use watch-reload glob... to add them to the list.

reflex-watch

The reflex-watch module is almost identical to modd-watch in function, but using reflex to watch files and run commands. (If reflex isn't installed, the watch command will try to install it to the project .deps directory using go install.)

The key differences are:

Modules for Golang-Using Projects

golang

The golang module makes it easy to fetch dependencies built with go (e.g. dnscontrol, modd, reflex, etc.), even on machines that don't have the right go version installed. In the simplest case, you would add lines like these to your .dkrc:

dk use: golang
golang 1.11.x   # use latest-available golang 1.11

And then use go install or other go commands as needed. Binaries will be built in .deps/bin, and the default GOROOT is .deps/go (unless you override these settings in .envrc).

The dk use: golang command gives your .dkrc access to the following functions:

Notice that if you are using direnv, simply going to your project directory will set up your environment such that the go on the PATH will be the version most recently requested via golang.

Also note that, by default, gimme installs requested go versions in a directory under the current user's home directory. You can change this behavior using the GIMME_VERSION_PREFIX and GIMME_ENV_PREFIX variables, which default to ~/.gimme/versions and ~/.gimme/envs, respectively. (For example, setting them to paths under .deps/.gimme/ would force the use of 100% project-local installs, at the cost of extra disk space and bootstrapping time.)

In addition to the above functions, the golang module also adds a dk golang subcommand, which:

Modules for Python-Using Projects

virtualenv

The virtualenv module makes it easy to use a Python virtual environment as part of your project, giving you a .deps/bin/python. Just dk use: virtualenv and you can access the have-virtualenv and create-virtualenv functions.

have-virtualenv returns success if you have an active virtualenv in .deps, while create-virtualenv creates a virtual environment with the specified options, as long as a virtualenv doesn't already exist. So, you might do this in your .dkrc to create a Python 3 virtualenv:

dk use: virtualenv
create-virtualenv -p python3

create-virtualenv passes along its arguments to virtualenv.py, automatically adding the .deps dir as the last argument, and activating the new virtualenv afterward. So you only need to specify any non-default options. The virtualenv will not be created if a virtualenv already exists; you'll have to delete the old one first (e.g. via script/clean) if you want to change the settings.

(Note: the default .envrc will always activate the virtualenv if it exists, so you do not need to do anything special to ensure that it will be used by subsequent runs of dk commands.)

Modules for PHP-Using Projects

composer

The composer module provides have-composer and require-composer functions, to check for or install composer-based command-line tools. have-composer sometool returns true if and only if vendor/bin/sometool exists under your project root, and require-composer sometool foo/bar will composer require --dev foo/bar if vendor/bin/sometool does not exist and a composer install doesn't create it. (Every argument to require-composer after the tool name is passed to composer require --dev, so you can specify any other composer options you like.)

peridot

The peridot module defines a dk peridot subcommand that is registered to run during dk test or script/test. The command runs peridot-php on the specs directory and pages the output if it doesn't fit on one screen. (You can set PERIDOT_SPECS to a different directory if your tests aren't under specs/, and you can set PERIDOT_OPTIONS to pass extra options to peridot.)

The peridot module integrates with the entr-watch module by adding $PERIDOT_SPECS/**/*.spec.php to the watch files list. (If you're using a different grep pattern, you'll want to redefine the peridot.files function accordingly.)

To use this module, just dk use: peridot in your .dkrc.

psysh-console

The psysh-console module implements a dk.console function to provide a script/console command that starts a psysh shell.

Modules for Projects Using bash

bash-kit

The bash-kit module adds a with-bash command you can use to run other commands in a docker container with a specified version of bash. Bashup projects use this to test compatibility with various bash versions. You must enable the module by including bash-kit in your .dkrc's dk use:, and the command will only work if you have access to docker with local volume support (to map the project directory into the container).

Running dk with-bash VERSION command ... runs dk command ... inside a docker container using the specified version of bash, so if you have the relevant devkit modules enabled, you can run test,watch, and even console, inside the container.

The following variables control the operation of the container:

Inside the docker container, a run-bash VERSION command... command is run first, allowing you to do any docker-specific setup (e.g. installing additional dependencies) by either overriding the command or defining a before handler for the run-bash event.

Note: the docker container will use .deps/.bash-VERSION as its /workdir/.deps, so that its installed dependencies can be platform-specific. Running clean inside a container will clean only the dependencies for that bash version, while running clean outside the container will wipe dependencies for both the base project and all bash versions used to that point.