├── docs ├── .gitignore ├── _sass │ ├── _base.scss │ ├── _layout.scss │ └── _syntax-highlighting.scss ├── Gemfile ├── assets │ ├── img │ │ └── build.png │ └── css │ │ └── main.scss ├── _includes │ ├── scripts.html │ ├── footer.html │ ├── head.html │ ├── sidebar.html │ └── header.html ├── _layouts │ ├── default.html │ └── doc.html ├── _config.yml ├── _docs │ ├── commands │ │ ├── help.md │ │ ├── init.md │ │ ├── graph.md │ │ ├── convert.md │ │ └── build.md │ ├── install.md │ ├── index.md │ └── tutorial.md ├── deploy ├── index.html └── Gemfile.lock ├── examples └── basic │ ├── foo.c │ ├── bar.c │ ├── foo.h │ ├── BUILD.lua │ └── button.json ├── .gitignore ├── source ├── util │ ├── package.d │ └── change.d └── button │ ├── watcher │ ├── kqueue.d │ ├── fsevents.d │ ├── windows.d │ ├── package.d │ └── inotify.d │ ├── cli │ ├── package.d │ ├── gc.d │ ├── help.d │ ├── status.d │ ├── clean.d │ ├── convert.d │ ├── init.d │ ├── graph.d │ ├── build.d │ └── options.d │ ├── handlers │ ├── tracer │ │ ├── package.d │ │ └── strace.d │ ├── package.d │ ├── recursive.d │ ├── gcc.d │ ├── dmd.d │ └── base.d │ ├── context.d │ ├── exceptions.d │ ├── edgedata.d │ ├── edge.d │ ├── app.d │ ├── events.d │ ├── textcolor.d │ ├── deps.d │ ├── loggers │ └── console.d │ ├── handler.d │ ├── command.d │ ├── rule.d │ ├── task.d │ └── resource.d ├── .travis.yml ├── dub.sdl ├── LICENSE └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /_site 3 | -------------------------------------------------------------------------------- /docs/_sass/_base.scss: -------------------------------------------------------------------------------- 1 | .text-muted { 2 | #color: #777; 3 | } 4 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /examples/basic/foo.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | 3 | int foo() 4 | { 5 | return 1; 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic/bar.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | 3 | int main() 4 | { 5 | return foo(); 6 | } 7 | -------------------------------------------------------------------------------- /docs/assets/img/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwhite/button/HEAD/docs/assets/img/build.png -------------------------------------------------------------------------------- /examples/basic/foo.h: -------------------------------------------------------------------------------- 1 | #ifndef FOO_H 2 | #define FOO_H 3 | 4 | int foo(); 5 | 6 | #endif // FOO_H 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | *.o 4 | *.swp 5 | /button 6 | /button-test-library 7 | dub.selections.json 8 | __test__library__ 9 | __pycache__/ 10 | .*.state 11 | *.bb.json 12 | -------------------------------------------------------------------------------- /docs/_includes/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /source/util/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module util; 7 | 8 | public import util.sqlite3; 9 | public import util.change; 10 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include header.html %} 6 | {{ content }} 7 | {% include footer.html %} 8 | {% include scripts.html %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | @charset "utf-8"; 4 | 5 | // Width of the content area 6 | $content-width: 1100px; 7 | $baseurl: {{ site.baseurl }}; 8 | 9 | // Import partials from `sass_dir` (defaults to `_sass`) 10 | @import "base", "layout", "syntax-highlighting"; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: d 5 | 6 | d: 7 | - dmd 8 | 9 | # Ruby is needed to build the Jekyll docs 10 | before_install: 11 | - sudo apt-get -qq update 12 | - sudo apt-get install -y ruby 13 | 14 | after_success: 15 | - cd docs && ./deploy 16 | -------------------------------------------------------------------------------- /examples/basic/BUILD.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2016 Jason White. MIT license. 3 | 4 | Description: 5 | Generates the build description for a simple "foobar" program. 6 | ]] 7 | 8 | local cc = require "rules.cc" 9 | 10 | cc.binary { 11 | name = "foobar", 12 | srcs = glob "*.c", 13 | } 14 | -------------------------------------------------------------------------------- /source/button/watcher/kqueue.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.watcher.kqueue; 7 | 8 | version (Windows): 9 | 10 | // TODO: Use kqueue to watch for file system changes. 11 | static assert(false, "Not implemented yet"); 12 | -------------------------------------------------------------------------------- /source/button/watcher/fsevents.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.watcher.fsevents; 7 | 8 | 9 | version (OSX): 10 | 11 | // TODO: Use FSEvents to watch for file system changes. 12 | static assert(false, "Not implemented yet"); 13 | -------------------------------------------------------------------------------- /source/button/watcher/windows.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.watcher.windows; 7 | 8 | version (Windows): 9 | 10 | // TODO: Use ReadDirectoryChangesW to watch for file system changes. 11 | static assert(false, "Not implemented yet"); 12 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "button" 2 | description "A build system that aims to be correct, scalable, and robust." 3 | copyright "Copyright © 2016, Jason White" 4 | authors "Jason White" 5 | license "MIT" 6 | 7 | dependency "fio" version=">=0.0.9" 8 | dependency "darg" version=">=0.0.4" 9 | 10 | sourcePaths "source/button" "source/util" 11 | 12 | libs "sqlite3" platform="posix" 13 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /source/button/cli/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.cli; 7 | 8 | public import button.cli.options; 9 | public import button.cli.build; 10 | public import button.cli.graph; 11 | public import button.cli.help; 12 | public import button.cli.status; 13 | public import button.cli.clean; 14 | public import button.cli.init; 15 | public import button.cli.gc; 16 | public import button.cli.convert; 17 | -------------------------------------------------------------------------------- /examples/basic/button.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": ["foo.c", "foo.h"], 4 | "task": [["gcc", "-c", "foo.c", "-o", "foo.o"]], 5 | "outputs": ["foo.o"] 6 | }, 7 | { 8 | "inputs": ["bar.c", "foo.h"], 9 | "task": [["gcc", "-c", "bar.c", "-o", "bar.o"]], 10 | "outputs": ["bar.o"] 11 | }, 12 | { 13 | "inputs": ["foo.o", "bar.o"], 14 | "task": [["gcc", "foo.o", "bar.o", "-o", "foobar"]], 15 | "outputs": ["foobar"] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Button 2 | description: > 3 | A recursive, universal build system. 4 | baseurl: "/button" 5 | url: "https://jasonwhite.github.io" # the base hostname & protocol for your site 6 | github: "https://github.com/jasonwhite/button" 7 | 8 | permalink: /:path/:basename 9 | 10 | collections: 11 | docs: 12 | output: true 13 | 14 | defaults: 15 | - scope: 16 | path: "" 17 | type: docs 18 | values: 19 | layout: doc 20 | category: intro 21 | 22 | markdown: kramdown 23 | -------------------------------------------------------------------------------- /docs/_docs/commands/help.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "button help" 3 | category: commands 4 | --- 5 | 6 | Prints help. 7 | 8 | ## Examples 9 | 10 | To get general help: 11 | 12 | $ button help 13 | 14 | To get help on a specific command: 15 | 16 | $ button help build 17 | 18 | Alternatively, the `--help` argument can be specified for a particular command: 19 | 20 | $ button build --help 21 | 22 | ## Positional Arguments 23 | 24 | * `command` 25 | 26 | The command to get help on. If not specified, prints general help. 27 | -------------------------------------------------------------------------------- /source/button/handlers/tracer/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Traces system calls in the process for precise dependency detection at the 8 | * cost of speed. This should be the fallback for a command if there is no 9 | * specialized handler for running it. 10 | */ 11 | module button.handlers.tracer; 12 | 13 | version (linux) 14 | { 15 | // Use strace on Linux. 16 | public import button.handlers.tracer.strace; 17 | } 18 | else 19 | { 20 | static assert(false, "Not implemented yet."); 21 | } 22 | -------------------------------------------------------------------------------- /docs/_layouts/doc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include header.html %} 6 |
7 |
8 | 11 |
12 |
13 | 14 | {{ content }} 15 |
16 |
17 |
18 |
19 | {% include footer.html %} 20 | {% include scripts.html %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }} - {{ site.title }}{% else %}{{ site.title }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/_docs/commands/init.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "button init" 3 | category: commands 4 | --- 5 | 6 | Initializes a directory with an initial build description. This is similar to 7 | Git's `init` command. 8 | 9 | Note that this command is not mandatory. It is just useful for quickly getting 10 | started on a new project. 11 | 12 | This command will *not* overwrite any existing files. 13 | 14 | ## Examples 15 | 16 | To create a `my_project` directory with a starting build description inside: 17 | 18 | $ button init my_project 19 | $ cd my_project 20 | $ button build 21 | 22 | To create a starting build description in the current directory: 23 | 24 | $ button init 25 | 26 | ## Positional Arguments 27 | 28 | * `dir` 29 | 30 | The directory to initialize. If not specified, defaults to the current 31 | directory. 32 | -------------------------------------------------------------------------------- /source/button/watcher/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Provides a range interface for watching the file system for changes. 8 | */ 9 | module button.watcher; 10 | 11 | version (linux) 12 | { 13 | public import button.watcher.inotify; 14 | } 15 | else version (Windows) 16 | { 17 | public import button.watcher.windows; 18 | } 19 | else version (OSX) 20 | { 21 | public import button.watcher.fsevents; 22 | } 23 | else version (FreeBSD) 24 | { 25 | public import button.watcher.kqueue; 26 | } 27 | else 28 | { 29 | // TODO: Provide a fallback of using the polling method. That is, 30 | // periodically stat all the watched files and check if they changed. 31 | static assert(false, "Not implemented on this platform"); 32 | } 33 | -------------------------------------------------------------------------------- /docs/_includes/sidebar.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /source/button/context.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.context; 7 | 8 | import std.parallelism : TaskPool; 9 | 10 | import button.events : Events; 11 | import button.state : BuildState; 12 | import button.textcolor : TextColor; 13 | 14 | /** 15 | * The build context. The members of this struct are very commonly used 16 | * throughout the build system. Thus, it is more convenient to bundle them 17 | * together and pass this struct around instead. 18 | * 19 | * Each of these values should be propagated to recursive runs of the build 20 | * system. That is, all child builds should use these settings instead of 21 | * constructing their own. 22 | */ 23 | struct BuildContext 24 | { 25 | string root; 26 | 27 | TaskPool pool; 28 | Events events; 29 | BuildState state; 30 | 31 | bool dryRun; 32 | 33 | // TODO: Move these settings into the logger. 34 | bool verbose; 35 | TextColor color; 36 | } 37 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /docs/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Used by the Travis-CI build to build the documentation and push the generated 4 | # HTML to the gh-pages branch. 5 | 6 | if [ "$TRAVIS_BRANCH" == "master" ]; then 7 | # Install dependencies required for building 8 | bundle install --path ~/.gem 9 | 10 | # Get the current state of the docs 11 | git clone --branch gh-pages https://github.com/${TRAVIS_REPO_SLUG}.git _site 12 | 13 | # Build the docs 14 | bundle exec jekyll build 15 | 16 | # Push the updated HTML 17 | if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 18 | cd _site 19 | git config credential.helper "store --file=.git/credentials" 20 | echo "https://${GH_TOKEN}:@github.com" > .git/credentials 21 | git config user.name "$GH_USER_NAME" 22 | git config user.email "$GH_USER_EMAIL" 23 | git config push.default simple 24 | git add --all . 25 | git commit -m "Auto update docs from travis-ci build $TRAVIS_BUILD_NUMBER" 26 | git push 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /source/button/handlers/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Command handler package. A command handler takes in a command line, executes 8 | * it, and returns a set of implicit inputs/outputs. Handlers can be called by 9 | * other handlers. 10 | * 11 | * This is useful for ad-hoc dependency detection. For example, to detect 12 | * inputs/outputs when running DMD, we modify the command line so it writes them 13 | * to a file which we then read in to determine the inputs/outputs. 14 | * 15 | * If there is no handler, we default to system call tracing. 16 | */ 17 | module button.handlers; 18 | 19 | // List of all handler types 20 | public import button.handlers.base : base = execute; 21 | public import button.handlers.recursive : recursive = execute; 22 | public import button.handlers.dmd : dmd = execute; 23 | public import button.handlers.gcc : gcc = execute; 24 | public import button.handlers.tracer : tracer = execute; 25 | -------------------------------------------------------------------------------- /source/button/cli/gc.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles command line arguments. 8 | */ 9 | module button.cli.gc; 10 | 11 | import button.cli.options : GCOptions, GlobalOptions; 12 | 13 | import io.text, io.file.stdio; 14 | 15 | import button.state, 16 | button.rule, 17 | button.graph, 18 | button.build, 19 | button.textcolor, 20 | button.exceptions; 21 | 22 | /** 23 | * Collects garbage. 24 | */ 25 | int collectGarbage(GCOptions opts, GlobalOptions globalOpts) 26 | { 27 | import std.getopt; 28 | 29 | immutable color = TextColor(colorOutput(opts.color)); 30 | 31 | try 32 | { 33 | string path = buildDescriptionPath(opts.path); 34 | 35 | auto state = new BuildState(path.stateName); 36 | } 37 | catch (BuildException e) 38 | { 39 | stderr.println(color.status, ":: ", color.error, 40 | "Error", color.reset, ": ", e.msg); 41 | return 1; 42 | } 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jason White 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/_sass/_layout.scss: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin-bottom: 100px; 8 | } 9 | 10 | body > .container { 11 | padding-top: 30px; 12 | } 13 | 14 | @media (min-width: $content-width) { 15 | .container { 16 | max-width: $content-width; 17 | } 18 | } 19 | 20 | .navbar { 21 | margin-bottom: 0; 22 | border-width: 0; 23 | } 24 | 25 | .navbar-inverse { 26 | background-color: #186fab; 27 | } 28 | 29 | .navbar-inverse a.navbar-brand { 30 | font-weight: bold; 31 | font-size: 22px; 32 | line-height: 22px; 33 | color: #fff; 34 | } 35 | 36 | .navbar-inverse .navbar-nav > li > a { 37 | color: #fff; 38 | } 39 | 40 | .navbar-inverse .navbar-nav > li > { 41 | a.active, a:hover { 42 | /*background-color: rgba(255, 255, 255, 0.15);*/ 43 | background-color: #135a8a; 44 | } 45 | } 46 | 47 | .jumbotron .container { 48 | max-width: $content-width; 49 | text-align: center; 50 | } 51 | 52 | #sidebar li.active a { 53 | border-right: 3px solid #337ab7; 54 | } 55 | 56 | .page-header { 57 | margin-top: 0; 58 | } 59 | 60 | .page-header > h1 { 61 | margin-top: 0; 62 | } 63 | 64 | .footer { 65 | position: absolute; 66 | bottom: 0; 67 | width: 100%; 68 | height: 60px; 69 | background-color: #f3f3f3; 70 | padding: 20px 15px; 71 | border-top: 1px solid #ccc; 72 | } 73 | 74 | /* 75 | * Markdown 76 | */ 77 | p > img { 78 | display: block; 79 | margin-left: auto; 80 | margin-right: auto; 81 | max-width: 100%; 82 | } 83 | -------------------------------------------------------------------------------- /source/button/exceptions.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Defines exception classes used in button 8 | * 9 | * Definitions of exception classes are separated into own module 10 | * to break module import cycles and reduce overall module inter-depdendency 11 | */ 12 | module button.exceptions; 13 | 14 | import std.exception : basicExceptionCtors; 15 | 16 | /** 17 | * Thrown when an invalid command name is given to $(D runCommand). 18 | */ 19 | class InvalidCommand : Exception 20 | { 21 | mixin basicExceptionCtors; 22 | } 23 | 24 | /** 25 | * Thrown if a command fails. 26 | */ 27 | class CommandError : Exception 28 | { 29 | int exitCode; 30 | 31 | this(int exitCode, string file = __FILE__, int line = __LINE__) 32 | { 33 | import std.format : format; 34 | 35 | super("Command failed with exit code %d".format(exitCode), file, line); 36 | 37 | this.exitCode = exitCode; 38 | } 39 | } 40 | 41 | /** 42 | * Exception that is thrown on invalid GCC deps syntax. 43 | */ 44 | class MakeParserError : Exception 45 | { 46 | mixin basicExceptionCtors; 47 | } 48 | 49 | /** 50 | * Thrown when an edge does not exist. 51 | */ 52 | class InvalidEdge : Exception 53 | { 54 | mixin basicExceptionCtors; 55 | } 56 | 57 | /** 58 | * An exception relating to the build. 59 | */ 60 | class BuildException : Exception 61 | { 62 | mixin basicExceptionCtors; 63 | } 64 | 65 | /** 66 | * Thrown if a task fails. 67 | */ 68 | class TaskError : Exception 69 | { 70 | mixin basicExceptionCtors; 71 | } 72 | -------------------------------------------------------------------------------- /docs/_docs/commands/graph.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "button graph" 3 | category: commands 4 | --- 5 | 6 | Produces output to be consumed by [GraphViz][]. This is useful for visualising 7 | the build graph. 8 | 9 | [GraphViz]: http://www.graphviz.org/ 10 | 11 | ## Examples 12 | 13 | To generate a PNG image of your build graph, run: 14 | 15 | $ button graph | dot -Tpng > build_graph.png 16 | 17 | Note that `dot` is part of [GraphViz][]. 18 | 19 | If running X11, you can also display an interactive graph: 20 | 21 | $ button graph | dot -Tx11 22 | 23 | ## Optional Arguments 24 | 25 | * `--file`, `-f ` 26 | 27 | Specifies the path to the build description. 28 | 29 | * `--changes`, `-C` 30 | 31 | Only display the subgraph that will be traversed in the next build. 32 | 33 | * `--cached` 34 | 35 | Displays the cached graph from the previous build. By default, changes to 36 | the build description are represented in the graph. 37 | 38 | * `--full` 39 | 40 | Displays the full name of each vertex. By default, the names of vertices are 41 | shown in condensed form. That is, resource paths are shortened to their 42 | basename and the display name of tasks (if available) are shown. If this 43 | option is specified, resource paths are shown in full and the full command 44 | line for a task is shown. This is off by default because it often makes 45 | vertices in the graph quite large. 46 | 47 | * `--edges`, `-e {explicit,implicit,both}` 48 | 49 | Type of edges to show. 50 | 51 | * `--threads`, `-j N` 52 | 53 | The number of threads to use. By default, the number of logical cores is 54 | used. 55 | 56 | -------------------------------------------------------------------------------- /source/button/edgedata.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.edgedata; 7 | 8 | /** 9 | * The type of an edge. 10 | */ 11 | enum EdgeType 12 | { 13 | /** 14 | * An explicit edge is one that was specified in the build description. 15 | * 16 | * The explicit specification of an edge from a task to a resource should be 17 | * considered a contract that must be fulfilled by the task. If the task 18 | * does not report that resource as an output, the task is marked as failed. 19 | */ 20 | explicit = 1 << 0, 21 | 22 | /** 23 | * An implicit edge is one that is reported by a task. 24 | * 25 | * The set of implicit edges should always be a superset of the set of 26 | * explicit edges. If this is not the case, it implies one of two problems: 27 | * 28 | * 1. A superfluous dependency is specified in the build description. 29 | * 2. The task is not reporting all dependencies. 30 | * 31 | * Case (1) causes no harm except over-building. However, case (2) should be 32 | * considered an error because explicit edges are a contract that the task 33 | * must fulfill. It is not possible to differentiate between these two 34 | * cases. Thus, the more conservative approach is taken to always consider 35 | * it an error if the set of explicit edges is not a subset of the set of 36 | * implicit edges. 37 | */ 38 | implicit = 1 << 1, 39 | 40 | /** 41 | * An edge is both explicit and implicit if it is in the build description 42 | * and reported by a task. 43 | */ 44 | both = explicit | implicit, 45 | } 46 | -------------------------------------------------------------------------------- /source/button/edge.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.edge; 7 | 8 | /** 9 | * An edge. Because the graph must be bipartite, an edge can never connect two 10 | * vertices of the same type. 11 | */ 12 | struct Edge(From, To) 13 | if (!is(From == To)) 14 | { 15 | From from; 16 | To to; 17 | 18 | /** 19 | * Compares two edges. 20 | */ 21 | int opCmp()(const auto ref typeof(this) rhs) const pure nothrow 22 | { 23 | if (this.from != rhs.from) 24 | return this.from < rhs.from ? -1 : 1; 25 | 26 | if (this.to != rhs.to) 27 | return this.to < rhs.to ? -1 : 1; 28 | 29 | return 0; 30 | } 31 | 32 | /** 33 | * Returns true if both edges are the same. 34 | */ 35 | bool opEquals()(const auto ref typeof(this) rhs) const pure 36 | { 37 | return from == rhs.from && 38 | to == rhs.to; 39 | } 40 | } 41 | 42 | /// Ditto 43 | struct Edge(From, To, Data) 44 | if (!is(From == To)) 45 | { 46 | From from; 47 | To to; 48 | 49 | Data data; 50 | 51 | /** 52 | * Compares two edges. 53 | */ 54 | int opCmp()(const auto ref typeof(this) rhs) const pure 55 | { 56 | if (this.from != rhs.from) 57 | return this.from < rhs.from ? -1 : 1; 58 | 59 | if (this.to != rhs.to) 60 | return this.to < rhs.to ? -1 : 1; 61 | 62 | if (this.data != rhs.data) 63 | return this.data < rhs.data ? -1 : 1; 64 | 65 | return 0; 66 | } 67 | 68 | /** 69 | * Returns true if both edges are the same. 70 | */ 71 | bool opEquals()(const auto ref typeof(this) rhs) const pure 72 | { 73 | return from == rhs.from && 74 | to == rhs.to && 75 | data == rhs.data; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/_docs/commands/convert.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "button convert" 3 | category: commands 4 | --- 5 | 6 | Converts the JSON build description to another format to be used by other build 7 | systems. 8 | 9 | Currently, only Bash output is supported. Support for other formats may come in 10 | the future if there is a need for them. 11 | 12 | This command can be useful to lower the barrier for contributing to a project. 13 | If someone has to install Button in order to build your project, that is yet 14 | another filter to potential contributors. To help mitigate this effect, you can 15 | generate a shell script using this command and commit the output to your 16 | repository. Ideally, generating the shell script and committing it should be 17 | automated with a continuous integration server. To aid this workflow, the output 18 | of `button convert` is deterministic so that it plays well with version control 19 | systems. 20 | 21 | ## Examples 22 | 23 | To generate and run a Bash script of the build description `button.json`: 24 | 25 | $ button convert build.sh 26 | $ ./build.sh 27 | 28 | If the build description is in another file, use the `-f` flag to specify where 29 | it is: 30 | 31 | $ button convert -f path.to.build.description.json build.sh 32 | $ ./build.sh 33 | 34 | ## Positional Arguments 35 | 36 | * `output` 37 | 38 | The file to write the output to. Depending on the format and platform, this 39 | file is made executable. 40 | 41 | ## Optional Arguments 42 | 43 | * `--file`, `-f ` 44 | 45 | Specifies the path to the build description. If not specified, Button 46 | searches for a file named `button.json` in the current directory and all 47 | parent directories. Thus, you can invoke this command in any subdirectory of 48 | your project. 49 | 50 | * `--format {bash}` 51 | 52 | Format of the build description to convert to. Defaults to `bash`. 53 | 54 | Only `bash` is currently supported. 55 | -------------------------------------------------------------------------------- /source/button/app.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Program entry point. 8 | */ 9 | import button.cli; 10 | import button.exceptions; 11 | 12 | import std.meta : AliasSeq; 13 | 14 | import io.text; 15 | import darg; 16 | 17 | /** 18 | * List of command functions. 19 | */ 20 | alias Commands = AliasSeq!( 21 | helpCommand, 22 | displayVersion, 23 | buildCommand, 24 | graphCommand, 25 | statusCommand, 26 | cleanCommand, 27 | collectGarbage, 28 | initCommand, 29 | convertCommand, 30 | ); 31 | 32 | version (unittest) 33 | { 34 | // Dummy main for unit testing. 35 | void main() {} 36 | } 37 | else 38 | { 39 | int main(const(string)[] args) 40 | { 41 | GlobalOptions opts; 42 | 43 | try 44 | { 45 | opts = parseArgs!GlobalOptions(args[1 .. $], Config.ignoreUnknown); 46 | } 47 | catch (ArgParseError e) 48 | { 49 | println("Error parsing arguments: ", e.msg, "\n"); 50 | println(globalUsage); 51 | return 1; 52 | } 53 | 54 | // Rewrite to "help" command. 55 | if (opts.help) 56 | { 57 | opts.args = (opts.command ? opts.command : "help") ~ opts.args; 58 | opts.command = "help"; 59 | } 60 | 61 | if (opts.command == "") 62 | { 63 | helpCommand(parseArgs!HelpOptions(opts.args), opts); 64 | return 1; 65 | } 66 | 67 | try 68 | { 69 | return runCommand!Commands(opts.command, opts); 70 | } 71 | catch (InvalidCommand e) 72 | { 73 | println(e.msg); 74 | return 1; 75 | } 76 | catch (ArgParseError e) 77 | { 78 | println("Error parsing arguments: ", e.msg, "\n"); 79 | displayHelp(opts.command); 80 | return 1; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [buildbadge]: https://travis-ci.org/jasonwhite/button.svg?branch=master 2 | [buildstatus]: https://travis-ci.org/jasonwhite/button 3 | 4 | # Button [![Build Status][buildbadge]][buildstatus] 5 | 6 | A build system that aims to be fast, correct, and elegantly simple. See the 7 | [documentation][] for more information. 8 | 9 | [documentation]: http://jasonwhite.github.io/button/ 10 | 11 | ## Features 12 | 13 | * Implicit dependency detection. 14 | * Correct incremental builds. 15 | * Can display a graph of the build. 16 | * Recursive. Can generate a build description as part of the build. 17 | * Very general. Does not make any assumptions about the structure of your 18 | project. 19 | * Detects and displays cyclic dependencies. 20 | * Detects race conditions. 21 | 22 | ## "Ugh! Another build system! [Why?!][relevant xkcd]" 23 | 24 | [relevant xkcd]: https://xkcd.com/927/ 25 | 26 | There are many, *many* other build systems out there. There are also many, 27 | *many* programming languages out there, but that hasn't stopped anyone from 28 | making even more. Advancing the state of a technology is all about incremental 29 | improvement. Button's raison d'être is to advance the state of build systems. 30 | Building software is a wildly complex task and we need a build system that can 31 | cope with that complexity without being too restrictive. 32 | 33 | Most build systems tend to suffer from one or more of the following problems: 34 | 35 | 1. They don't do correct incremental builds. 36 | 2. They don't correctly track changes to the build description. 37 | 3. They don't scale well with large projects (100,000+ source files). 38 | 4. They are language-specific or aren't general enough to be widely used 39 | outside of a niche community. 40 | 5. They are tied to a domain specific language. 41 | 42 | Button is designed such that it can solve all of these problems. Read the 43 | [overview][] in the documentation to find out how. 44 | 45 | [overview]: http://jasonwhite.github.io/button/docs/ 46 | 47 | ## License 48 | 49 | [MIT License](/LICENSE) 50 | -------------------------------------------------------------------------------- /source/button/events.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Classes for receiving events from the build system. This is the general 8 | * mechanism through which information is logged. 9 | */ 10 | module button.events; 11 | 12 | import button.task; 13 | import button.state; 14 | import core.time : Duration; 15 | 16 | /** 17 | * Interface for handling build system events. This can be used for logging or 18 | * visualization purposes. 19 | * 20 | * Examples of what can be done with this include: 21 | * - Showing build progress in the terminal. 22 | * - Generating a JSON log file to be analyzed later. 23 | * - Sending events to a web interface for visualization. 24 | * - Generating a Gantt chart of task durations to see critical paths. 25 | */ 26 | interface Events 27 | { 28 | /** 29 | * Called when a build has started. 30 | */ 31 | void buildStarted(); 32 | 33 | /** 34 | * Called when a build has completed successfully. 35 | */ 36 | void buildSucceeded(Duration duration); 37 | 38 | /** 39 | * Called when a build has failed with the exception that was thrown. 40 | */ 41 | void buildFailed(Duration duration, Exception e); 42 | 43 | /** 44 | * Called when a task has started. Returns a new event handler for tasks. 45 | * 46 | * Parameters: 47 | * worker = The node on which the task is running. This is guaranteed to 48 | * be between 0 and the size of the task pool. 49 | * task = The task itself. 50 | */ 51 | void taskStarted(size_t worker, const ref Task task); 52 | 53 | /** 54 | * Called when a task has completed successfully. 55 | */ 56 | void taskSucceeded(size_t worker, const ref Task task, 57 | Duration duration); 58 | 59 | /** 60 | * Called when a task has failed. 61 | */ 62 | void taskFailed(size_t worker, const ref Task task, Duration duration, 63 | const Exception e); 64 | 65 | /** 66 | * Called when a chunk of output is received from the task. 67 | */ 68 | void taskOutput(size_t worker, in ubyte[] chunk); 69 | } 70 | -------------------------------------------------------------------------------- /source/button/cli/help.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles command line arguments. 8 | */ 9 | module button.cli.help; 10 | 11 | import button.cli.options; 12 | 13 | import io.text, io.file.stdio; 14 | 15 | import darg; 16 | 17 | int displayHelp(string command) 18 | { 19 | import io.text; 20 | import std.traits : getUDAs; 21 | 22 | foreach (Options; OptionsList) 23 | { 24 | alias commands = getUDAs!(Options, Command); 25 | foreach (c; commands) 26 | { 27 | if (c.name == command) 28 | { 29 | enum usage = usageString!Options("button "~ commands[0].name); 30 | 31 | alias descriptions = getUDAs!(Options, Description); 32 | static if(descriptions.length > 0) 33 | enum help = helpString!Options(descriptions[0].description); 34 | else 35 | enum help = helpString!Options(); 36 | 37 | static if (usage !is null) 38 | println(usage); 39 | static if (help !is null) 40 | println(help); 41 | 42 | return 0; 43 | } 44 | } 45 | } 46 | 47 | printfln("No help available for '%s'.", command); 48 | return 1; 49 | } 50 | 51 | private immutable string generalHelp = q"EOS 52 | The most commonly used commands are: 53 | build Builds based on changes. 54 | graph Writes the build description in GraphViz format. 55 | help Prints help on a specific command. 56 | 57 | Use 'button help ' to get help on a specific command. 58 | EOS"; 59 | 60 | /** 61 | * Display help information. 62 | */ 63 | int helpCommand(HelpOptions opts, GlobalOptions globalOpts) 64 | { 65 | if (opts.command) 66 | return displayHelp(opts.command); 67 | 68 | println(globalUsage); 69 | println(globalHelp); 70 | println(generalHelp); 71 | return 0; 72 | } 73 | 74 | /** 75 | * Display version information. 76 | */ 77 | int displayVersion(VersionOptions opts, GlobalOptions globalOpts) 78 | { 79 | stdout.println("button version 0.1.0"); 80 | return 0; 81 | } 82 | -------------------------------------------------------------------------------- /source/button/textcolor.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Helper module for colorizing terminal output. 8 | */ 9 | module button.textcolor; 10 | 11 | /* 12 | * Black 0;30 Dark Gray 1;30 13 | * Red 0;31 Light Red 1;31 14 | * Green 0;32 Light Green 1;32 15 | * Brown/Orange 0;33 Yellow 1;33 16 | * Blue 0;34 Light Blue 1;34 17 | * Purple 0;35 Light Purple 1;35 18 | * Cyan 0;36 Light Cyan 1;36 19 | * Light Gray 0;37 White 1;37 20 | */ 21 | 22 | private 23 | { 24 | immutable black = "\033[0;30m", boldBlack = "\033[1;30m", 25 | red = "\033[0;31m", boldRed = "\033[1;31m", 26 | green = "\033[0;32m", boldGreen = "\033[1;32m", 27 | orange = "\033[0;33m", boldOrange = "\033[1;33m", 28 | blue = "\033[0;34m", boldBlue = "\033[1;34m", 29 | purple = "\033[0;35m", boldPurple = "\033[1;35m", 30 | cyan = "\033[0;36m", boldCyan = "\033[1;36m", 31 | lightGray = "\033[0;37m", boldLightGray = "\033[1;37m"; 32 | 33 | immutable bold = "\033[1m"; 34 | immutable reset = "\033[0m"; 35 | 36 | immutable success = boldGreen; 37 | immutable error = boldRed; 38 | immutable warning = boldOrange; 39 | immutable status = blue; 40 | } 41 | 42 | struct TextColor 43 | { 44 | private bool _enabled; 45 | 46 | this(bool enabled) 47 | { 48 | _enabled = enabled; 49 | } 50 | 51 | @property 52 | immutable(string) opDispatch(string name)() const pure nothrow 53 | { 54 | if (!_enabled) 55 | return ""; 56 | 57 | return mixin(name); 58 | } 59 | } 60 | 61 | /** 62 | * Returns true if the output is capable of being colorized. 63 | */ 64 | version (Windows) 65 | { 66 | enum colorizable = false; 67 | } 68 | else 69 | { 70 | bool colorizable() 71 | { 72 | // FIXME: Check on a stream-by-stream basis. If stderr is a terminal, 73 | // but stdout isn't, then this fails to do the correct thing. 74 | import io.file.stdio : stdout; 75 | return stdout.isTerminal; 76 | } 77 | } 78 | 79 | /** 80 | * Returns true if the output should be colored based on the given option. 81 | */ 82 | bool colorOutput(string option) 83 | { 84 | switch (option) 85 | { 86 | case "always": 87 | return true; 88 | case "never": 89 | return false; 90 | default: 91 | return colorizable; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: default 4 | --- 5 |
6 |
7 |

Button

8 |

A universal build system to build your software at the push of a 9 | button.

10 |

Get Started »

11 |
12 |
13 | 14 |
15 |

Button is currently only supported on Linux, but 16 | there are plans to also support OS X and Windows soonish.

17 |

Features

18 |
19 |
20 |

Implicit Dependency Detection

21 |

There is no need to exhaustively list dependencies (such as C++ 22 | header files). Dependencies for any language can be determined 23 | automatically.

24 |
25 |
26 |

Correct Incremental Builds

27 |

Outputs removed from the build are automatically removed from 28 | disk. Changes are determined by file contents, not just by 29 | timestamps. Combined with implicit dependency detection, correct 30 | incremental builds can be ensured.

31 |
32 |
33 |

Recursive Builds

34 |

Any build task can also be a build system. This simple feature is 35 | incredibly powerful. As part of the build, you can generate your build 36 | description with, say, a high-level scripting language like Lua.

37 |
38 |
39 |
40 |
41 |

Hands-free Building

42 |

Dependencies can be watched to automatically trigger a build when 43 | they change. This can help speed up the edit-compile-test 44 | development cycle.

45 |
46 |
47 |

Visualize Builds

48 |

A graph of the task graph can be displayed. This helps give you 49 | an immediate and intuitive understanding of the structure of your 50 | build.

51 |
52 |
53 |

Language Independent

54 |

This is not a language-specific build system. While there are 55 | convenient abstractions for building common languages, there are no 56 | restrictions on building projects not covered by those 57 | abstractions.

58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /source/button/cli/status.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Displays status about the build. 8 | */ 9 | module button.cli.status; 10 | 11 | import button.cli.options : StatusOptions, GlobalOptions; 12 | 13 | import std.getopt; 14 | import std.range : empty; 15 | import std.array : array; 16 | import std.algorithm : sort, map, filter; 17 | 18 | import io.text, 19 | io.file; 20 | 21 | import button.task; 22 | import button.resource; 23 | import button.state; 24 | import button.build; 25 | import button.textcolor; 26 | import button.exceptions; 27 | 28 | int statusCommand(StatusOptions opts, GlobalOptions globalOpts) 29 | { 30 | import std.parallelism : TaskPool, totalCPUs; 31 | 32 | if (opts.threads == 0) 33 | opts.threads = totalCPUs; 34 | 35 | auto pool = new TaskPool(opts.threads - 1); 36 | scope (exit) pool.finish(true); 37 | 38 | immutable color = TextColor(colorOutput(opts.color)); 39 | 40 | try 41 | { 42 | string path = buildDescriptionPath(opts.path); 43 | auto state = new BuildState(path.stateName); 44 | 45 | state.begin(); 46 | scope (exit) state.rollback(); 47 | 48 | if (!opts.cached) 49 | path.syncState(state, pool); 50 | 51 | printfln("%d resources and %d tasks total", 52 | state.length!Resource, 53 | state.length!Task); 54 | 55 | displayPendingResources(state, color); 56 | displayPendingTasks(state, color); 57 | } 58 | catch (BuildException e) 59 | { 60 | stderr.println(":: Error: ", e.msg); 61 | return 1; 62 | } 63 | 64 | return 0; 65 | } 66 | 67 | void displayPendingResources(BuildState state, TextColor color) 68 | { 69 | auto resources = state.enumerate!Resource 70 | .filter!(v => v.update()) 71 | .array 72 | .sort(); 73 | 74 | if (resources.empty) 75 | { 76 | println("No resources have been modified."); 77 | } 78 | else 79 | { 80 | printfln("%d modified resource(s):\n", resources.length); 81 | 82 | foreach (v; resources) 83 | println(" ", color.blue, v, color.reset); 84 | 85 | println(); 86 | } 87 | } 88 | 89 | void displayPendingTasks(BuildState state, TextColor color) 90 | { 91 | auto tasks = state.pending!Task.array; 92 | 93 | if (tasks.empty) 94 | { 95 | println("No tasks are pending."); 96 | } 97 | else 98 | { 99 | printfln("%d pending task(s):\n", tasks.length); 100 | 101 | foreach (v; tasks) 102 | println(" ", color.blue, state[v].toPrettyString, color.reset); 103 | 104 | println(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /source/button/cli/clean.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Command to delete outputs. 8 | */ 9 | module button.cli.clean; 10 | 11 | import button.cli.options : CleanOptions, GlobalOptions; 12 | 13 | import io.text, io.file.stdio; 14 | 15 | import button.state, 16 | button.rule, 17 | button.graph, 18 | button.build, 19 | button.textcolor, 20 | button.exceptions; 21 | 22 | /** 23 | * Deletes outputs. 24 | */ 25 | int cleanCommand(CleanOptions opts, GlobalOptions globalOpts) 26 | { 27 | import std.getopt; 28 | import std.file : remove; 29 | 30 | immutable color = TextColor(colorOutput(opts.color)); 31 | 32 | try 33 | { 34 | string path = buildDescriptionPath(opts.path); 35 | string statePath = path.stateName; 36 | 37 | auto state = new BuildState(statePath); 38 | 39 | { 40 | state.begin(); 41 | scope (success) 42 | { 43 | if (opts.dryRun) 44 | state.rollback(); 45 | else 46 | state.commit(); 47 | } 48 | 49 | scope (failure) 50 | state.rollback(); 51 | 52 | clean(state, opts.dryRun); 53 | } 54 | 55 | // Close the database before (potentially) deleting it. 56 | state.close(); 57 | 58 | if (opts.purge) 59 | { 60 | println("Deleting `", statePath, "`"); 61 | remove(statePath); 62 | } 63 | } 64 | catch (BuildException e) 65 | { 66 | stderr.println(color.status, ":: ", color.error, 67 | "Error", color.reset, ": ", e.msg); 68 | return 1; 69 | } 70 | 71 | return 0; 72 | } 73 | 74 | /** 75 | * Deletes all outputs from the file system. 76 | */ 77 | void clean(BuildState state, bool dryRun) 78 | { 79 | import io.text, io.file.stdio; 80 | import std.range : takeOne; 81 | import button.resource : Resource; 82 | 83 | foreach (id; state.enumerate!(Index!Resource)) 84 | { 85 | if (state.degreeIn(id) > 0) 86 | { 87 | auto r = state[id]; 88 | 89 | println("Deleting `", r, "`"); 90 | 91 | r.remove(dryRun); 92 | 93 | // Update the database with the new status of the resource. 94 | state[id] = r; 95 | 96 | // We want to build this the next time around, so mark its task as 97 | // pending. 98 | auto incoming = state 99 | .incoming!(NeighborIndex!(Index!Resource))(id) 100 | .takeOne; 101 | assert(incoming.length == 1, 102 | "Output resource has does not have 1 incoming edge! "~ 103 | "Something has gone horribly wrong!"); 104 | state.addPending(incoming[0].vertex); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /source/button/deps.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.deps; 7 | 8 | import button.resource; 9 | 10 | /** 11 | * Format for dependencies received from a task over a pipe. 12 | */ 13 | align(4) struct Dependency 14 | { 15 | /** 16 | * Status of the resource. 17 | * 18 | * Can be: 19 | * 0: Status is unknown. 20 | * 1: Resource does not exist. 21 | * 2: The resource is a file. 22 | * 3: The resource is a directory. 23 | */ 24 | uint status; 25 | 26 | /** 27 | * SHA-256 checksum of the contents of the resource. If unknown or not 28 | * computed, this should be set to 0. In such a case, the parent build 29 | * system will compute the value when needed. 30 | * 31 | * For files, this is the checksum of the file contents. For directories, 32 | * this is the checksum of the paths in the sorted directory listing. 33 | */ 34 | ubyte[32] checksum; 35 | 36 | /** 37 | * Length of the name. 38 | */ 39 | uint length; 40 | 41 | /** 42 | * Name of the resource that can be used to lookup the data. Length is given 43 | * by the length member. 44 | * 45 | * This is usually a file or directory path. The path does not need to be 46 | * normalized. The path is assumed to be relative to the associated task's 47 | * working directory. 48 | */ 49 | char[0] name; 50 | } 51 | 52 | unittest 53 | { 54 | static assert(Dependency.sizeof == 40); 55 | } 56 | 57 | /** 58 | * Range of resources received from a child process. 59 | */ 60 | struct Deps 61 | { 62 | private 63 | { 64 | immutable(void)[] buf; 65 | 66 | Resource _current; 67 | bool _empty; 68 | } 69 | 70 | this(immutable(void)[] buf) 71 | { 72 | this.buf = buf; 73 | popFront(); 74 | } 75 | 76 | Resource front() inout 77 | { 78 | return _current; 79 | } 80 | 81 | bool empty() const pure nothrow 82 | { 83 | return _empty; 84 | } 85 | 86 | void popFront() 87 | { 88 | import std.datetime : SysTime; 89 | 90 | if (buf.length == 0) 91 | { 92 | _empty = true; 93 | return; 94 | } 95 | 96 | if (buf.length < Dependency.sizeof) 97 | throw new Exception("Received partial dependency buffer"); 98 | 99 | auto dep = *cast(Dependency*)buf[0 .. Dependency.sizeof]; 100 | 101 | immutable totalSize = Dependency.sizeof + dep.length; 102 | 103 | string name = cast(string)buf[Dependency.sizeof .. totalSize]; 104 | 105 | _current = Resource( 106 | name, 107 | cast(Resource.Status)dep.status, 108 | dep.checksum 109 | ); 110 | 111 | buf = buf[totalSize .. $]; 112 | } 113 | } 114 | 115 | /** 116 | * Convenience function for returning a range of resources. 117 | */ 118 | Deps deps(immutable(void)[] buf) 119 | { 120 | return Deps(buf); 121 | } 122 | -------------------------------------------------------------------------------- /source/button/loggers/console.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * General logging of events to the console. 8 | */ 9 | module button.loggers.console; 10 | 11 | import core.time : Duration; 12 | 13 | import io.file.stream; 14 | import io.text; 15 | 16 | import button.events; 17 | import button.task; 18 | import button.state; 19 | import button.textcolor : TextColor; 20 | 21 | final class ConsoleLogger : Events 22 | { 23 | private 24 | { 25 | import std.range : Appender; 26 | 27 | // Console streams to write to. 28 | File stdout; 29 | File stderr; 30 | 31 | // True if output should be verbose. 32 | bool verbose; 33 | 34 | TextColor color; 35 | 36 | // List of current task output. There is one appender per worker in the 37 | // thread pool. 38 | Appender!(ubyte[])[] output; 39 | } 40 | 41 | this(File stdout, File stderr, bool verbose, size_t poolSize) 42 | { 43 | this.stdout = stdout; 44 | this.stderr = stderr; 45 | this.verbose = verbose; 46 | this.color = TextColor(true); 47 | 48 | // The +1 is to accommodate index 0 which is used for threads not in the 49 | // pool. 50 | this.output.length = poolSize + 1; 51 | } 52 | 53 | void buildStarted() 54 | { 55 | } 56 | 57 | void buildSucceeded(Duration duration) 58 | { 59 | } 60 | 61 | void buildFailed(Duration duration, Exception e) 62 | { 63 | } 64 | 65 | void taskStarted(size_t worker, const ref Task task) 66 | { 67 | output[worker].clear(); 68 | } 69 | 70 | private void printTaskOutput(size_t worker) 71 | { 72 | auto data = output[worker].data; 73 | 74 | stdout.write(data); 75 | 76 | if (data.length > 0 && data[$-1] != '\n') 77 | stdout.print("⏎\n"); 78 | } 79 | 80 | private void printTaskTail(size_t worker, Duration duration) 81 | { 82 | import core.time : Duration; 83 | 84 | if (verbose) 85 | { 86 | stdout.println(color.status, " ➥ Time taken: ", color.reset, 87 | cast(Duration)duration); 88 | } 89 | } 90 | 91 | void taskSucceeded(size_t worker, const ref Task task, 92 | Duration duration) 93 | { 94 | synchronized (this) 95 | { 96 | stdout.println(color.status, " > ", color.reset, 97 | task.toPrettyString(verbose)); 98 | 99 | printTaskOutput(worker); 100 | printTaskTail(worker, duration); 101 | } 102 | } 103 | 104 | void taskFailed(size_t worker, const ref Task task, Duration duration, 105 | const Exception e) 106 | { 107 | import std.string : wrap; 108 | 109 | synchronized (this) 110 | { 111 | stdout.println(color.status, " > ", color.error, 112 | task.toPrettyString(verbose), color.reset); 113 | 114 | printTaskOutput(worker); 115 | printTaskTail(worker, duration); 116 | 117 | enum indent = " "; 118 | 119 | stdout.print(color.status, " ➥ ", color.error, "Error: ", 120 | color.reset, wrap(e.msg, 80, "", indent, 4)); 121 | } 122 | } 123 | 124 | void taskOutput(size_t worker, in ubyte[] chunk) 125 | { 126 | output[worker].put(chunk); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /docs/_sass/_syntax-highlighting.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Syntax highlighting styles 3 | */ 4 | .highlight { 5 | background: #fff; 6 | 7 | .highlighter-rouge & { 8 | background: #fcfcfc; 9 | padding: 1em; 10 | } 11 | 12 | .c { color: #998; font-style: italic } // Comment 13 | .err { color: #a61717; background-color: #e3d2d2 } // Error 14 | .k { font-weight: bold } // Keyword 15 | .o { font-weight: bold } // Operator 16 | .cm { color: #998; font-style: italic } // Comment.Multiline 17 | .cp { color: #999; font-weight: bold } // Comment.Preproc 18 | .c1 { color: #998; font-style: italic } // Comment.Single 19 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special 20 | .gd { color: #000; background-color: #fdd } // Generic.Deleted 21 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific 22 | .ge { font-style: italic } // Generic.Emph 23 | .gr { color: #a00 } // Generic.Error 24 | .gh { color: #999 } // Generic.Heading 25 | .gi { color: #000; background-color: #dfd } // Generic.Inserted 26 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific 27 | .go { color: #888 } // Generic.Output 28 | .gp { color: #555 } // Generic.Prompt 29 | .gs { font-weight: bold } // Generic.Strong 30 | .gu { color: #aaa } // Generic.Subheading 31 | .gt { color: #a00 } // Generic.Traceback 32 | .kc { font-weight: bold } // Keyword.Constant 33 | .kd { font-weight: bold } // Keyword.Declaration 34 | .kp { font-weight: bold } // Keyword.Pseudo 35 | .kr { font-weight: bold } // Keyword.Reserved 36 | .kt { color: #458; font-weight: bold } // Keyword.Type 37 | .m { color: #099 } // Literal.Number 38 | .s { color: #d14 } // Literal.String 39 | .na { color: #008080 } // Name.Attribute 40 | .nb { color: #0086B3 } // Name.Builtin 41 | .nc { color: #458; font-weight: bold } // Name.Class 42 | .no { color: #008080 } // Name.Constant 43 | .ni { color: #800080 } // Name.Entity 44 | .ne { color: #900; font-weight: bold } // Name.Exception 45 | .nf { color: #900; font-weight: bold } // Name.Function 46 | .nn { color: #555 } // Name.Namespace 47 | .nt { color: #000080 } // Name.Tag 48 | .nv { color: #008080 } // Name.Variable 49 | .ow { font-weight: bold } // Operator.Word 50 | .w { color: #bbb } // Text.Whitespace 51 | .mf { color: #099 } // Literal.Number.Float 52 | .mh { color: #099 } // Literal.Number.Hex 53 | .mi { color: #099 } // Literal.Number.Integer 54 | .mo { color: #099 } // Literal.Number.Oct 55 | .sb { color: #d14 } // Literal.String.Backtick 56 | .sc { color: #d14 } // Literal.String.Char 57 | .sd { color: #d14 } // Literal.String.Doc 58 | .s2 { color: #d14 } // Literal.String.Double 59 | .se { color: #d14 } // Literal.String.Escape 60 | .sh { color: #d14 } // Literal.String.Heredoc 61 | .si { color: #d14 } // Literal.String.Interpol 62 | .sx { color: #d14 } // Literal.String.Other 63 | .sr { color: #009926 } // Literal.String.Regex 64 | .s1 { color: #d14 } // Literal.String.Single 65 | .ss { color: #990073 } // Literal.String.Symbol 66 | .bp { color: #999 } // Name.Builtin.Pseudo 67 | .vc { color: #008080 } // Name.Variable.Class 68 | .vg { color: #008080 } // Name.Variable.Global 69 | .vi { color: #008080 } // Name.Variable.Instance 70 | .il { color: #099 } // Literal.Number.Integer.Long 71 | } 72 | -------------------------------------------------------------------------------- /source/button/handler.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * This is the root command handler. That is, this decides which command handler 8 | * to use. 9 | */ 10 | module button.handler; 11 | 12 | import button.resource; 13 | import button.context; 14 | 15 | import button.handlers; 16 | import button.command; 17 | import button.task; 18 | 19 | alias Handler = void function( 20 | ref BuildContext ctx, 21 | const(string)[] args, 22 | string workDir, 23 | ref Resources inputs, 24 | ref Resources outputs 25 | ); 26 | 27 | immutable Handler[string] handlers; 28 | shared static this() 29 | { 30 | handlers = [ 31 | "button": &recursive, 32 | "button-lua": &base, 33 | "dmd": &dmd, 34 | "gcc": &gcc, 35 | "g++": &gcc, 36 | "c++": &gcc, 37 | ]; 38 | } 39 | 40 | /** 41 | * Returns a handler appropriate for the given arguments. 42 | * 43 | * In general, this simply looks at the base name of the first argument and 44 | * determines the tool based on that. 45 | */ 46 | Handler selectHandler(const(string)[] args) 47 | { 48 | import std.uni : toLower; 49 | import std.path : baseName, filenameCmp; 50 | 51 | if (args.length) 52 | { 53 | auto name = baseName(args[0]); 54 | 55 | // Need case-insensitive comparison on Windows. 56 | version (Windows) 57 | name = name.toLower; 58 | 59 | if (auto p = name in handlers) 60 | return *p; 61 | } 62 | 63 | return &tracer; 64 | } 65 | 66 | void execute( 67 | ref BuildContext ctx, 68 | const(string)[] args, 69 | string workDir, 70 | ref Resources inputs, 71 | ref Resources outputs 72 | ) 73 | { 74 | auto handler = selectHandler(args); 75 | 76 | handler(ctx, args, workDir, inputs, outputs); 77 | } 78 | 79 | /** 80 | * Executes the task. 81 | */ 82 | Task.Result execute(const Task task, ref BuildContext ctx) 83 | { 84 | import std.array : appender; 85 | 86 | // FIXME: Use a set instead? 87 | auto inputs = appender!(Resource[]); 88 | auto outputs = appender!(Resource[]); 89 | 90 | foreach (command; task.commands) 91 | { 92 | auto result = command.execute(ctx, task.workingDirectory); 93 | 94 | // FIXME: Commands may have temporary inputs and outputs. For 95 | // example, if one command creates a file and a later command 96 | // deletes it, it should not end up in either of the input or output 97 | // sets. 98 | inputs.put(result.inputs); 99 | outputs.put(result.outputs); 100 | } 101 | 102 | return Task.Result(inputs.data, outputs.data); 103 | } 104 | 105 | /** 106 | * Executes the command. 107 | */ 108 | Command.Result execute(const Command command, ref BuildContext ctx, 109 | string workDir) 110 | { 111 | import std.path : buildPath; 112 | import std.datetime.stopwatch : StopWatch, AutoStart; 113 | import button.handler : executeHandler = execute; 114 | 115 | auto inputs = Resources(ctx.root, workDir); 116 | auto outputs = Resources(ctx.root, workDir); 117 | 118 | auto sw = StopWatch(AutoStart.yes); 119 | 120 | executeHandler( 121 | ctx, 122 | command.args, 123 | buildPath(ctx.root, workDir), 124 | inputs, outputs 125 | ); 126 | 127 | return Command.Result(inputs.data, outputs.data, sw.peek()); 128 | } 129 | -------------------------------------------------------------------------------- /source/button/cli/convert.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles the 'convert' command. 8 | */ 9 | module button.cli.convert; 10 | 11 | import std.parallelism : TaskPool; 12 | 13 | import button.cli.options : ConvertOptions, ConvertFormat, GlobalOptions; 14 | 15 | import button.build; 16 | import button.resource; 17 | import button.task; 18 | import button.exceptions; 19 | 20 | import io.file, io.text; 21 | 22 | /** 23 | * Converts the JSON build description to another format. 24 | * 25 | * This is useful for converting the build to a shell script, for example. 26 | */ 27 | int convertCommand(ConvertOptions opts, GlobalOptions globalOpts) 28 | { 29 | string path; 30 | 31 | try 32 | { 33 | path = buildDescriptionPath(opts.path); 34 | } 35 | catch (BuildException e) 36 | { 37 | stderr.println(e.msg); 38 | return 1; 39 | } 40 | catch (SysException e) 41 | { 42 | stderr.println(e.msg); 43 | return 1; 44 | } 45 | 46 | // TODO: Add Batch output with proper error handling 47 | 48 | final switch (opts.type) 49 | { 50 | case ConvertFormat.bash: 51 | return convertToBash(path, opts.output); 52 | } 53 | } 54 | 55 | /** 56 | * A header explaining what this file is. 57 | */ 58 | private immutable bashHeader = q"EOS 59 | # This file was automatically generated by Button. Do not modify it. 60 | EOS"; 61 | 62 | bool visitResource(File* f, Resource v, 63 | size_t degreeIn, size_t degreeChanged) 64 | { 65 | // Nothing needs to happen here. Just unconditionally continue on to the 66 | // next vertex in the graph. 67 | return true; 68 | } 69 | 70 | bool bashVisitTask(File* f, Task v, 71 | size_t degreeIn, size_t degreeChanged) 72 | { 73 | import button.command : escapeShellArg; 74 | 75 | f.println(); 76 | 77 | if (v.display.length) 78 | f.println("# ", v.display); 79 | 80 | if (v.workingDirectory.length) 81 | f.println("pushd -- ", v.workingDirectory.escapeShellArg); 82 | 83 | foreach (command; v.commands) 84 | f.println(command.toPrettyString); 85 | 86 | if (v.workingDirectory.length) 87 | f.println("popd"); 88 | 89 | // Unconditionally continue on to the next vertex in the graph. 90 | return true; 91 | } 92 | 93 | /** 94 | * Converts the build description to Bash. 95 | */ 96 | private int convertToBash(string input, string output) 97 | { 98 | import std.parallelism : TaskPool; 99 | 100 | auto f = File(output, FileFlags.writeEmpty); 101 | 102 | version (Posix) 103 | { 104 | // Make the output executable. This is a workaround until the mode can 105 | // be changed in the File() constructor. 106 | import core.sys.posix.sys.stat : chmod; 107 | import std.internal.cstring : tempCString; 108 | sysEnforce(chmod(output.tempCString(), 0b111_101_101) == 0, 109 | "Failed to make script executable"); 110 | } 111 | 112 | f.println("#!/bin/bash"); 113 | f.print(bashHeader); 114 | 115 | // Stop the build when a command fails. 116 | f.println("set -xe -o pipefail"); 117 | 118 | // Traverse the graph single-threaded, writing out the commands 119 | auto g = input.rules.graph(); 120 | 121 | auto pool = new TaskPool(0); 122 | scope (exit) pool.finish(true); 123 | 124 | g.traverse!(visitResource, bashVisitTask)(&f, pool); 125 | 126 | return 0; 127 | } 128 | -------------------------------------------------------------------------------- /source/button/cli/init.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Initializes a directory with an initial build description. This is useful 8 | * to quickly get up and running when creating first creating a build 9 | * description for the first time on a project. 10 | * 11 | * This makes the assumption that you want to use Lua as your build description 12 | * language and creates an initial BUILD.lua file for you. 13 | */ 14 | module button.cli.init; 15 | 16 | import io; 17 | 18 | import button.cli.options : InitOptions, GlobalOptions; 19 | 20 | /** 21 | * Contents of .gitignore 22 | */ 23 | immutable gitIgnoreContents = q"EOS 24 | # Generated Button files 25 | .BUILD.lua.json 26 | .*.json.state 27 | EOS"; 28 | 29 | /** 30 | * Path to the root build description template. 31 | */ 32 | immutable rootTemplate = "button.json"; 33 | 34 | /** 35 | * Contents of the root build description. 36 | * 37 | * In general this file should always be a wrapper for generating a build 38 | * description. Thus, this should never need to be modified by hand. 39 | * 40 | * Here, we assume we want to use button-lua to generate the build description. 41 | */ 42 | immutable rootTemplateContents = q"EOS 43 | [ 44 | { 45 | "inputs": ["BUILD.lua"], 46 | "task": [["button-lua", "BUILD.lua", "-o", ".BUILD.lua.json"]], 47 | "outputs": [".BUILD.lua.json"] 48 | }, 49 | { 50 | "inputs": [".BUILD.lua.json"], 51 | "task": [["button", "build", "--color=always", "-f", ".BUILD.lua.json"]], 52 | "outputs": [".BUILD.lua.json.state"] 53 | } 54 | ] 55 | EOS"; 56 | 57 | /** 58 | * Path to the Lua build description template. 59 | */ 60 | immutable luaTemplate = "BUILD.lua"; 61 | 62 | /** 63 | * Contents of the BUILD.lua file. 64 | * 65 | * TODO: Give more a more useful starting point. This should include: 66 | * 1. A link to the documentation (when it finally exists). 67 | * 2. A simple hello world example. 68 | */ 69 | immutable luaTemplateContents = q"EOS 70 | --[[ 71 | This is the top-level build description. This is where you either create 72 | build rules or delegate to other Lua scripts to create build rules. 73 | 74 | See the documentation for more information on how to get started. 75 | ]] 76 | 77 | EOS"; 78 | 79 | 80 | int initCommand(InitOptions opts, GlobalOptions globalOpts) 81 | { 82 | import std.path : buildPath; 83 | import std.file : FileException, mkdirRecurse; 84 | 85 | try 86 | { 87 | // Ensure the directory and its parents exist. This will be used to 88 | // store the root build description and the build state. 89 | mkdirRecurse(opts.dir); 90 | } 91 | catch (FileException e) 92 | { 93 | println(e.msg); 94 | return 1; 95 | } 96 | 97 | try 98 | { 99 | File(buildPath(opts.dir, ".gitignore"), FileFlags.writeNew) 100 | .write(gitIgnoreContents); 101 | } 102 | catch (SysException e) 103 | { 104 | // Don't care if it already exists. 105 | } 106 | 107 | try 108 | { 109 | // Create the root build description 110 | File(buildPath(opts.dir, rootTemplate), FileFlags.writeNew) 111 | .write(rootTemplateContents); 112 | 113 | // Create BUILD.lua 114 | File(buildPath(opts.dir, luaTemplate), FileFlags.writeNew) 115 | .write(luaTemplateContents); 116 | } 117 | catch (SysException e) 118 | { 119 | println("Error: ", e.msg); 120 | println(" Looks like you already ran `button init`."); 121 | return 1; 122 | } 123 | 124 | return 0; 125 | } 126 | -------------------------------------------------------------------------------- /docs/_docs/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | category: intro 4 | order: 1 5 | --- 6 | 7 | Table of contents: 8 | 9 | * TOC 10 | {:toc} 11 | 12 | --- 13 | 14 | Button consists of two main components: 15 | 16 | 1. `button`: The build system itself. 17 | 2. `button-lua`: The build description generator from Lua scripts. 18 | 19 | ## System Requirements 20 | 21 | Supported platforms: 22 | 23 | * Linux 24 | 25 | Unsupported platforms: 26 | 27 | * OS X 28 | * Windows 29 | 30 | Supported for OS X and Windows will be coming in the future. 31 | 32 | ## Installation Packages 33 | 34 | Installation packages are rather sparse at the moment. If your distribution is 35 | not supported below, please see the [Compiling From 36 | Source](#compiling-from-source) section. If you decide to create a package for 37 | your distribution, a pull request adding it to this section would be very 38 | appreciated. 39 | 40 | ### Arch Linux 41 | 42 | Install [button][button-aur] from the Arch User Repository (AUR). 43 | 44 | [button-aur]: https://aur.archlinux.org/packages/button/ 45 | 46 | ## Compiling From Source 47 | 48 | ### Installing Dependencies 49 | 50 | To build, you'll need [Git][], [DMD][] (the D compiler), and [DUB][] (the D 51 | package manager). 52 | 53 | On Arch Linux, these can be installed with: 54 | 55 | $ sudo pacman -Sy git dlang dub 56 | 57 | [Git]: https://git-scm.com/ 58 | [DMD]: http://dlang.org/download.html 59 | [DUB]: http://code.dlang.org/download 60 | 61 | ### Building `button` 62 | 63 | 1. Get the source: 64 | 65 | ```bash 66 | $ git clone https://github.com/jasonwhite/button.git && cd button 67 | ``` 68 | 69 | 2. Build it: 70 | 71 | ```bash 72 | $ dub build --build=release 73 | ``` 74 | 75 | There should now be a `button` executable in the current directory. Copy it to a 76 | directory that is in your `$PATH` and run it to make sure it is working: 77 | 78 | $ button help 79 | 80 | ### Building `button-lua` 81 | 82 | `button-lua` is written in C++ and thus the build process is a little different. 83 | 84 | 1. Get the source: 85 | 86 | ```bash 87 | $ git clone --recursive https://github.com/jasonwhite/button-lua.git && cd button-lua 88 | ``` 89 | 90 | 2. Build it: 91 | 92 | ``` 93 | $ make 94 | ``` 95 | 96 | There should now be a `button-lua` executable in the current directory. Copy it 97 | to a directory that is in your `$PATH` and run it to make sure it is working: 98 | 99 | $ button-lua 100 | Usage: button-lua