├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _docs │ ├── commands │ │ ├── build.md │ │ ├── convert.md │ │ ├── graph.md │ │ ├── help.md │ │ └── init.md │ ├── index.md │ ├── install.md │ └── tutorial.md ├── _includes │ ├── footer.html │ ├── head.html │ ├── header.html │ ├── scripts.html │ └── sidebar.html ├── _layouts │ ├── default.html │ └── doc.html ├── _sass │ ├── _base.scss │ ├── _layout.scss │ └── _syntax-highlighting.scss ├── assets │ ├── css │ │ ├── bootstrap.min.css │ │ └── main.scss │ ├── img │ │ └── build.png │ └── js │ │ └── bootstrap.min.js ├── deploy └── index.html ├── dub.sdl ├── examples └── basic │ ├── BUILD.lua │ ├── bar.c │ ├── button.json │ ├── foo.c │ └── foo.h └── source ├── button ├── app.d ├── build.d ├── cli │ ├── build.d │ ├── clean.d │ ├── convert.d │ ├── gc.d │ ├── graph.d │ ├── help.d │ ├── init.d │ ├── options.d │ ├── package.d │ └── status.d ├── command.d ├── context.d ├── deps.d ├── edge.d ├── edgedata.d ├── events.d ├── exceptions.d ├── graph.d ├── handler.d ├── handlers │ ├── base.d │ ├── dmd.d │ ├── gcc.d │ ├── package.d │ ├── recursive.d │ └── tracer │ │ ├── package.d │ │ └── strace.d ├── loggers │ └── console.d ├── resource.d ├── rule.d ├── state.d ├── task.d ├── textcolor.d └── watcher │ ├── fsevents.d │ ├── inotify.d │ ├── kqueue.d │ ├── package.d │ └── windows.d └── util ├── change.d ├── package.d └── sqlite3.d /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /_site 3 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | RedCloth (4.2.9) 5 | activesupport (4.2.6) 6 | i18n (~> 0.7) 7 | json (~> 1.7, >= 1.7.7) 8 | minitest (~> 5.1) 9 | thread_safe (~> 0.3, >= 0.3.4) 10 | tzinfo (~> 1.1) 11 | addressable (2.4.0) 12 | coffee-script (2.4.1) 13 | coffee-script-source 14 | execjs 15 | coffee-script-source (1.10.0) 16 | colorator (0.1) 17 | ethon (0.9.0) 18 | ffi (>= 1.3.0) 19 | execjs (2.6.0) 20 | faraday (0.9.2) 21 | multipart-post (>= 1.2, < 3) 22 | ffi (1.9.10) 23 | gemoji (2.1.0) 24 | github-pages (77) 25 | RedCloth (= 4.2.9) 26 | github-pages-health-check (= 1.1.0) 27 | jekyll (= 3.0.5) 28 | jekyll-coffeescript (= 1.0.1) 29 | jekyll-feed (= 0.5.1) 30 | jekyll-gist (= 1.4.0) 31 | jekyll-github-metadata (= 1.11.1) 32 | jekyll-mentions (= 1.1.2) 33 | jekyll-paginate (= 1.1.0) 34 | jekyll-redirect-from (= 0.10.0) 35 | jekyll-sass-converter (= 1.3.0) 36 | jekyll-seo-tag (= 1.4.0) 37 | jekyll-sitemap (= 0.10.0) 38 | jekyll-textile-converter (= 0.1.0) 39 | jemoji (= 0.6.2) 40 | kramdown (= 1.10.0) 41 | liquid (= 3.0.6) 42 | listen (= 3.0.6) 43 | mercenary (~> 0.3) 44 | rdiscount (= 2.1.8) 45 | redcarpet (= 3.3.3) 46 | rouge (= 1.10.1) 47 | terminal-table (~> 1.4) 48 | github-pages-health-check (1.1.0) 49 | addressable (~> 2.3) 50 | net-dns (~> 0.8) 51 | octokit (~> 4.0) 52 | public_suffix (~> 1.4) 53 | typhoeus (~> 0.7) 54 | html-pipeline (2.4.0) 55 | activesupport (>= 2, < 5) 56 | nokogiri (>= 1.4) 57 | i18n (0.7.0) 58 | jekyll (3.0.5) 59 | colorator (~> 0.1) 60 | jekyll-sass-converter (~> 1.0) 61 | jekyll-watch (~> 1.1) 62 | kramdown (~> 1.3) 63 | liquid (~> 3.0) 64 | mercenary (~> 0.3.3) 65 | rouge (~> 1.7) 66 | safe_yaml (~> 1.0) 67 | jekyll-coffeescript (1.0.1) 68 | coffee-script (~> 2.2) 69 | jekyll-feed (0.5.1) 70 | jekyll-gist (1.4.0) 71 | octokit (~> 4.2) 72 | jekyll-github-metadata (1.11.1) 73 | octokit (~> 4.0) 74 | jekyll-mentions (1.1.2) 75 | html-pipeline (~> 2.3) 76 | jekyll (~> 3.0) 77 | jekyll-paginate (1.1.0) 78 | jekyll-redirect-from (0.10.0) 79 | jekyll (>= 2.0) 80 | jekyll-sass-converter (1.3.0) 81 | sass (~> 3.2) 82 | jekyll-seo-tag (1.4.0) 83 | jekyll (~> 3.0) 84 | jekyll-sitemap (0.10.0) 85 | jekyll-textile-converter (0.1.0) 86 | RedCloth (~> 4.0) 87 | jekyll-watch (1.4.0) 88 | listen (~> 3.0, < 3.1) 89 | jemoji (0.6.2) 90 | gemoji (~> 2.0) 91 | html-pipeline (~> 2.2) 92 | jekyll (>= 3.0) 93 | json (1.8.3) 94 | kramdown (1.10.0) 95 | liquid (3.0.6) 96 | listen (3.0.6) 97 | rb-fsevent (>= 0.9.3) 98 | rb-inotify (>= 0.9.7) 99 | mercenary (0.3.6) 100 | mini_portile2 (2.0.0) 101 | minitest (5.8.4) 102 | multipart-post (2.0.0) 103 | net-dns (0.8.0) 104 | nokogiri (1.6.7.2) 105 | mini_portile2 (~> 2.0.0.rc2) 106 | octokit (4.3.0) 107 | sawyer (~> 0.7.0, >= 0.5.3) 108 | public_suffix (1.5.3) 109 | rb-fsevent (0.9.7) 110 | rb-inotify (0.9.7) 111 | ffi (>= 0.5.0) 112 | rdiscount (2.1.8) 113 | redcarpet (3.3.3) 114 | rouge (1.10.1) 115 | safe_yaml (1.0.4) 116 | sass (3.4.22) 117 | sawyer (0.7.0) 118 | addressable (>= 2.3.5, < 2.5) 119 | faraday (~> 0.8, < 0.10) 120 | terminal-table (1.5.2) 121 | thread_safe (0.3.5) 122 | typhoeus (0.8.0) 123 | ethon (>= 0.8.0) 124 | tzinfo (1.2.2) 125 | thread_safe (~> 0.1) 126 | 127 | PLATFORMS 128 | ruby 129 | 130 | DEPENDENCIES 131 | github-pages 132 | 133 | BUNDLED WITH 134 | 1.11.2 135 | -------------------------------------------------------------------------------- /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/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "button build" 3 | category: commands 4 | --- 5 | 6 | Brings the build state up-to-date. This includes: 7 | 8 | 1. Updating the internal task graph based on structural changes to the build 9 | description. 10 | 11 | 2. Deleting outputs that should no longer get built. For example, if the task 12 | `gcc -c foo.c -o foo.o` was removed from the build description, the output 13 | `foo.o` will be deleted from disk. 14 | 15 | 3. Checking for changes to resources. A resource is considered "changed" if 16 | both its last modification time changed *and* the checksum of its contents 17 | changed. 18 | 19 | 4. Running tasks based on changed inputs. 20 | 21 | This will be the command you will use 99% of the time. It is equivalent to an 22 | "incremental build". Although you should never need to do a complete rebuild, 23 | you can do so by running `button clean` followed by `button build`. 24 | 25 | ## Examples 26 | 27 | If your root build description `button.json` is in the current working directory 28 | or one of its parent directories, simply run: 29 | 30 | $ button build 31 | 32 | If instead your root build description is named something other than 33 | `button.json`, such as `my_build_description.json`, run: 34 | 35 | $ button build --file my_build_description.json 36 | 37 | Note that the working directory of Button will change to the directory of the 38 | root build description before running the build. 39 | 40 | ## Optional Arguments 41 | 42 | * `--file`, `-f ` 43 | 44 | Specifies the path to the build description. If not specified, Button 45 | searches for a file named `button.json` in the current directory and all 46 | parent directories. Thus, you can invoke this command in any subdirectory of 47 | your project. 48 | 49 | * `--dryrun`, `-n` 50 | 51 | Don't make any functional changes; just print what might happen. This can be 52 | useful when refactoring the build description. 53 | 54 | * `--threads`, `-j N` 55 | 56 | The number of threads to use when executing tasks or checking for changes. 57 | By default, the number of logical cores is used. 58 | 59 | * `--color {auto,never,always}` 60 | 61 | When to colorize the output. If set to `auto` (the default), output is 62 | colorized if the standard output pipe [refers to a terminal][isatty]. 63 | 64 | * `--verbose`, `-v` 65 | 66 | Display additional information such as how long each task took to complete 67 | or the full command line to tasks. 68 | 69 | * `--autopilot` 70 | 71 | After completing the initial build, continue watching for changes to inputs 72 | and building again as necessary. This can be very useful to speed up the 73 | edit-compile-test cycle of development. 74 | 75 | * `--watchdir ` 76 | 77 | Directory to watch for changes in. Used in conjunction with `--autopilot`. 78 | Since FUSE does not work with inotify, this is useful to use when building 79 | in a union file system where the "lower" file system contains source code 80 | and the "upper" file system is where output files are written to. If 81 | building in the upper file system, inotify cannot receive change events. 82 | However, setting `--watchdir` to the lower file system (so long as it isn't 83 | also a FUSE file system) will work as expected. 84 | 85 | * `--delay ` 86 | 87 | Used in conjunction with `--autopilot`. The number of milliseconds to wait 88 | for additional changes. That is, if after an initial change notification is 89 | received, the number of milliseconds to wait before starting a build. 90 | 91 | For example, suppose you have `button build --autopilot` running and do a 92 | `git pull`. Instead of running a build for every file Git changes, the 93 | changes are *accumulated* and a build is run after `git pull` is completely 94 | done. 95 | 96 | By default, there is a delay of 50 milliseconds. This should be long enough 97 | for computer-performed tasks (such as `git pull`) and short enough to be 98 | imperceptible to a human saving changes in a text editor. 99 | 100 | [isatty]: http://linux.die.net/man/3/isatty 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/_docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Overview" 3 | category: intro 4 | order: 0 5 | permalink: /:collection/ 6 | --- 7 | 8 | Table of contents: 9 | 10 | * TOC 11 | {:toc} 12 | 13 | --- 14 | 15 | Button is a very general, elegantly simple, and powerful build system. This 16 | document gives a high-level overview of what Button is, what it can do, and how 17 | it works. 18 | 19 | ## Introduction 20 | 21 | If you don't already know what a build system is, it is a tool to automate the 22 | steps necessary to translate source code to deliverables. Well known tools in 23 | this domain include: 24 | 25 | * [Make][], [MSBuild][] 26 | * [Ant][], [Maven][], [Gradle][] 27 | * [Bazel][], [Buck][], [Pants][] 28 | 29 | Note that Button is *not* a project generator, package manager, or continuous 30 | integration server. However, it is certainly an excellent base to build these 31 | things off of. 32 | 33 | [Make]: https://www.gnu.org/software/make/ 34 | [MSBuild]: https://github.com/Microsoft/msbuild 35 | [Ant]: http://ant.apache.org/ 36 | [Maven]: https://maven.apache.org/ 37 | [Gradle]: http://gradle.org/ 38 | [Bazel]: http://bazel.io/ 39 | [Buck]: https://buckbuild.com/ 40 | [Pants]: http://pantsbuild.github.io/ 41 | 42 | ## Features 43 | 44 | Button has some pretty neat features: 45 | 46 | * Fast and correct incremental builds. 47 | * Implicit dependency detection. 48 | * The ability to generate the build description as part of the build. 49 | * Can run builds automatically when something changes. 50 | 51 | Because it is general enough to be able to build a project written in any 52 | language, Button is particularly useful for building multi-language projects. 53 | Many other build systems are tailored for a particular language. While this can 54 | be a good thing for single-language projects, it can also become very 55 | restrictive as the project gets more complicated. 56 | 57 | ## How It Works 58 | 59 | In order to understand how Button works, it is imperative to understand at a 60 | high level its underlying data structure and how that data structure is operated 61 | on. 62 | 63 | ### The Build Graph 64 | 65 | At the heart of this build system is a bipartite directed acyclic graph: 66 | 67 | ![Build Graph]({{ site.baseurl }}/assets/img/build.png) 68 | 69 | Lets just call this the *build graph* because the proper mathematical term is a 70 | mouthful. The build graph is [bipartite][] because it can be partitioned into 71 | two types of nodes: *resources* and *tasks*. In the figure above, the resources 72 | and tasks are shown as ellipses and rectangles, respectively. A resource is some 73 | file and a task is some program to execute. Resources are the inputs and outputs 74 | of tasks. 75 | 76 | In order to build, we simply traverse the graph starting at the top and work our 77 | way down while executing tasks. Of course, tasks that don't depend on each other 78 | are executed in parallel. Furthermore, if a resource hasn't been modified, the 79 | task it leads to will not be executed. 80 | 81 | There are two important restrictions on the structure of the build graph: 82 | 83 | 1. It must be [acyclic][]. That is, there must be no path in the graph where 84 | you can traverse the same edge twice. 85 | 2. Output resources can have one parent task. This helps prevent race 86 | conditions. 87 | 88 | Both of these cases are detected automatically and result in an error. 89 | 90 | [bipartite]: https://en.wikipedia.org/wiki/Bipartite_graph 91 | [acyclic]: https://en.wikipedia.org/wiki/Directed_acyclic_graph 92 | 93 | ### The Build Description 94 | 95 | The build graph is stored internally and created from the *build description*. 96 | The build description is simply a JSON file containing a list of *rules*: 97 | 98 | ```json 99 | [ 100 | { 101 | "inputs": ["foo.c", "baz.h"], 102 | "task": [["gcc", "-c", "foo.c", "-o", "foo.o"]], 103 | "outputs": ["foo.o"] 104 | }, 105 | { 106 | "inputs": ["bar.c", "baz.h"], 107 | "task": [["gcc", "-c", "bar.c", "-o", "bar.o"]], 108 | "outputs": ["bar.o"] 109 | }, 110 | { 111 | "inputs": ["foo.o", "bar.o"], 112 | "task": [["gcc", "foo.o", "bar.o", "-o", "foobar"]], 113 | "outputs": ["foobar"] 114 | } 115 | ] 116 | ``` 117 | 118 | A rule consists of a list of inputs, a task, and a list of outputs. Connecting 119 | these rules together forms the build graph as shown in the previous section. 120 | 121 | When the build description is modified and we run the build again with `button 122 | build`, the internal build graph is incrementally updated with the changes. If a 123 | rule is added to the build description, then it is added to the build graph and 124 | the task is marked as "out of date" so that it gets unconditionally executed. 125 | If, on the other hand, a rule is removed from the build description, then it is 126 | removed from the build graph and all of its outputs are deleted from the file 127 | system. This ensures there are no extraneous files laying around to interfere 128 | with the build. 129 | 130 | Of course, you probably don't want to modify the JSON build description file by 131 | hand. For anything but trivial examples, it would be far too cumbersome and 132 | error-prone to do so. The next three sections describe the solution to this 133 | problem -- generating the build description. 134 | 135 | ### Implicit Dependencies 136 | 137 | An implicit dependency (as opposed to an explicit dependency) is one that is not 138 | specified in the build description, but discovered by running a task. The 139 | canonical example of implicit dependencies are C/C++ header files. It is tedious 140 | to explicitly specify these in the build description, but even worse, it is 141 | error-prone. In general, the set of explicit dependencies will be a subset of 142 | the implicit dependencies. If this is not the case, then either (1) you've 143 | over specified dependencies or (2) implicit dependencies have not been correctly 144 | detected. 145 | 146 | Any task in the build graph, when executed, can tell Button about its input and 147 | output resources. This is a generalized way of allowing implicit dependency 148 | detection. Button has fast ad hoc detection for various compilers but falls back 149 | to tracing system calls for programs it doesn't know about. This all happens 150 | behind the scenes without you having to do anything special. 151 | 152 | #### Restrictions 153 | 154 | There is one immutable rule about implicit dependencies that cannot be violated: 155 | **if added to the build graph, an implicit dependency must not change the build 156 | order**. If this rule is violated, the task will fail, Cthulhu will be summoned, 157 | and Button will tell you to explicitly add the would-be dependency to the build 158 | description. (If you don't do it, Cthulhu will *find* you). 159 | 160 | Allowing an implicit dependency to change the build order while the build is 161 | running could lead to incorrect builds. More often, however, it is a mistake in 162 | the build description and, thus, this scenario is strictly forbidden. 163 | 164 | ### Recursive Builds 165 | 166 | Any task in the build graph can also be a build system. That is, Button can 167 | recursively run itself as part of the build. Doing this with `make` is generally 168 | [considered harmful][RMCH] because it throws correct incremental builds out the 169 | window. However, this is only because `make` doesn't know about the dependencies 170 | of a sub-`make`. This is not a problem for Button because it knows how to send 171 | information about implicit dependencies to a parent Button process. By 172 | publishing implicit dependencies to the parent, the child build system can be 173 | executed again if any of its inputs change. 174 | 175 | [RMCH]: http://lcgapp.cern.ch/project/architecture/recursive_make.pdf 176 | 177 | ### Building the Build Description 178 | 179 | Since we can correctly do recursive builds, we can also generate the build 180 | description with, say, a scripting language as part of the build. The program 181 | [`button-lua`][button-lua] is provided for this purpose. As the name might 182 | imply, it uses the lightweight [Lua][] scripting language to specify build 183 | descriptions at a high level. For example, this considerably more terse script 184 | (`BUILD.lua`) can generate the JSON build description from the earlier section: 185 | 186 | ```lua 187 | local cc = require "rules.cc" 188 | 189 | cc.binary { 190 | name = "foobar", 191 | srcs = glob "*.c", 192 | } 193 | ``` 194 | 195 | Unfortunately, we must still have an *upper* JSON build description as this is 196 | what the parent Button needs to read in: 197 | 198 | ```json 199 | [ 200 | { 201 | "inputs": ["BUILD.lua"], 202 | "task": ["button-lua", "BUILD.lua", "-o", "build.button.json"], 203 | "outputs": ["build.button.json"] 204 | }, 205 | { 206 | "inputs": ["build.button.json"], 207 | "task": ["button", "update", "--color=always", "-f", "build.button.json"], 208 | "outputs": [".build.button.json.state"] 209 | } 210 | ] 211 | ``` 212 | 213 | Fortunately, this rarely requires modification as most of the changes will be in 214 | the Lua script as your project grows. 215 | 216 | See the [tutorial][] to learn more. 217 | 218 | [button-lua]: https://github.com/jasonwhite/button-lua 219 | [Lua]: https://www.lua.org/ 220 | 221 | [tutorial]: {{ "/docs/tutorial" | prepend: site.baseurl }} 222 | -------------------------------------------------------------------------------- /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 2 | 3 | -------------------------------------------------------------------------------- /docs/_includes/sidebar.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /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/_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/_sass/_base.scss: -------------------------------------------------------------------------------- 1 | .text-muted { 2 | #color: #777; 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/assets/img/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwhite/button/d8d746a3dcd134ab5b570cc2555ea84dac8f1202/docs/assets/img/build.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/basic/bar.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | 3 | int main() 4 | { 5 | return foo(); 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/basic/foo.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | 3 | int foo() 4 | { 5 | return 1; 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic/foo.h: -------------------------------------------------------------------------------- 1 | #ifndef FOO_H 2 | #define FOO_H 3 | 4 | int foo(); 5 | 6 | #endif // FOO_H 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/button/cli/build.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles the 'build' command. 8 | */ 9 | module button.cli.build; 10 | 11 | import std.parallelism : TaskPool; 12 | 13 | import button.cli.options : BuildOptions, GlobalOptions; 14 | 15 | import io.text, io.file; 16 | 17 | import button.state; 18 | import button.rule; 19 | import button.graph; 20 | import button.build; 21 | import button.resource; 22 | import button.task; 23 | import button.textcolor; 24 | import button.events; 25 | import button.watcher; 26 | import button.context; 27 | import button.exceptions; 28 | 29 | /** 30 | * Updates the build. 31 | * 32 | * All outputs are brought up-to-date based on their inputs. If '--autopilot' is 33 | * specified, once the build finishes, we watch for changes to inputs and run 34 | * another build. 35 | */ 36 | int buildCommand(BuildOptions opts, GlobalOptions globalOpts) 37 | { 38 | import std.parallelism : totalCPUs; 39 | import std.path : dirName, absolutePath; 40 | import button.loggers.console; 41 | 42 | if (opts.threads == 0) 43 | opts.threads = totalCPUs; 44 | 45 | auto pool = new TaskPool(opts.threads - 1); 46 | scope (exit) pool.finish(true); 47 | 48 | immutable color = TextColor(colorOutput(opts.color)); 49 | 50 | auto events = new ConsoleLogger(stdout, stderr, opts.verbose, pool.size); 51 | 52 | string path; 53 | BuildState state; 54 | 55 | try 56 | { 57 | path = buildDescriptionPath(opts.path); 58 | state = new BuildState(path.stateName); 59 | } 60 | catch (BuildException e) 61 | { 62 | stderr.println(color.status, ":: ", color.error, 63 | "Error", color.reset, ": ", e.msg); 64 | return 1; 65 | } 66 | 67 | auto context = BuildContext(absolutePath(dirName(path)), pool, events, 68 | state, opts.dryRun, opts.verbose, color); 69 | 70 | if (!opts.autopilot) 71 | { 72 | return doBuild(context, path); 73 | } 74 | else 75 | { 76 | // Do the initial build, checking for changes the old-fashioned way. 77 | doBuild(context, path); 78 | 79 | return doAutoBuild(context, path, opts.watchDir, opts.delay); 80 | } 81 | } 82 | 83 | int doBuild(ref BuildContext ctx, string path) 84 | { 85 | import std.datetime.stopwatch : StopWatch, AutoStart; 86 | 87 | auto sw = StopWatch(AutoStart.yes); 88 | 89 | scope (exit) 90 | { 91 | import std.conv : to; 92 | import core.time : Duration; 93 | sw.stop(); 94 | 95 | if (ctx.verbose) 96 | { 97 | println(ctx.color.status, ":: Total time taken: ", ctx.color.reset, 98 | cast(Duration)sw.peek()); 99 | } 100 | } 101 | 102 | try 103 | { 104 | ctx.state.begin(); 105 | scope (exit) 106 | { 107 | if (ctx.dryRun) 108 | ctx.state.rollback(); 109 | else 110 | ctx.state.commit(); 111 | } 112 | 113 | syncBuildState(ctx, path); 114 | 115 | if (ctx.verbose) 116 | println(ctx.color.status, ":: Checking for changes...", ctx.color.reset); 117 | 118 | queueChanges(ctx.state, ctx.pool, ctx.color); 119 | 120 | update(ctx); 121 | } 122 | catch (BuildException e) 123 | { 124 | stderr.println(ctx.color.status, ":: ", ctx.color.error, 125 | "Error", ctx.color.reset, ": ", e.msg); 126 | return 1; 127 | } 128 | catch (Exception e) 129 | { 130 | stderr.println(ctx.color.status, ":: ", ctx.color.error, 131 | "Build failed!", ctx.color.reset, 132 | " See the output above for details."); 133 | if (ctx.verbose) 134 | println(ctx.color.status, ":: ", e.toString()); 135 | 136 | return 1; 137 | } 138 | 139 | return 0; 140 | } 141 | 142 | int doAutoBuild(ref BuildContext ctx, string path, 143 | string watchDir, size_t delay) 144 | { 145 | println(ctx.color.status, ":: Waiting for changes...", ctx.color.reset); 146 | 147 | ctx.state.begin(); 148 | scope (exit) 149 | { 150 | if (ctx.dryRun) 151 | ctx.state.rollback(); 152 | else 153 | ctx.state.commit(); 154 | } 155 | 156 | foreach (changes; ChangeChunks(ctx.state, watchDir, delay)) 157 | { 158 | try 159 | { 160 | size_t changed = 0; 161 | 162 | foreach (v; changes) 163 | { 164 | // Check if the resource contents actually changed 165 | auto r = ctx.state[v]; 166 | 167 | if (r.update()) 168 | { 169 | ctx.state.addPending(v); 170 | ctx.state[v] = r; 171 | ++changed; 172 | } 173 | } 174 | 175 | if (changed > 0) 176 | { 177 | println(ctx.color.status, ":: Change detected. Building...", 178 | ctx.color.reset); 179 | syncBuildState(ctx, path); 180 | update(ctx); 181 | println(ctx.color.status, ":: Waiting for changes...", 182 | ctx.color.reset); 183 | } 184 | } 185 | catch (BuildException e) 186 | { 187 | stderr.println(ctx.color.status, ":: ", ctx.color.error, 188 | "Error", ctx.color.reset, ": ", e.msg); 189 | continue; 190 | } 191 | catch (Exception e) 192 | { 193 | stderr.println(ctx.color.status, ":: ", ctx.color.error, 194 | "Build failed!", ctx.color.reset, 195 | " See the output above for details."); 196 | continue; 197 | } 198 | } 199 | 200 | // Unreachable 201 | } 202 | 203 | /** 204 | * Updates the database with any changes to the build description. 205 | */ 206 | void syncBuildState(ref BuildContext ctx, string path) 207 | { 208 | // TODO: Don't store the build description in the database. The parent build 209 | // system should store the change state of the build description and tell 210 | // the child which input resources have changed upon an update. 211 | auto r = ctx.state[BuildState.buildDescId]; 212 | r.path = path; 213 | if (r.update()) 214 | { 215 | if (ctx.verbose) 216 | println(ctx.color.status, 217 | ":: Build description changed. Syncing with the database...", 218 | ctx.color.reset); 219 | 220 | path.syncState(ctx.state, ctx.pool); 221 | 222 | // Update the build description resource 223 | ctx.state[BuildState.buildDescId] = r; 224 | } 225 | } 226 | 227 | /** 228 | * Builds pending vertices. 229 | */ 230 | void update(ref BuildContext ctx) 231 | { 232 | import std.array : array; 233 | import std.algorithm.iteration : filter; 234 | 235 | auto resources = ctx.state.pending!Resource.array; 236 | auto tasks = ctx.state.pending!Task.array; 237 | 238 | if (resources.length == 0 && tasks.length == 0) 239 | { 240 | if (ctx.verbose) 241 | { 242 | println(ctx.color.status, ":: ", ctx.color.success, 243 | "Nothing to do. Everything is up to date.", ctx.color.reset); 244 | } 245 | 246 | return; 247 | } 248 | 249 | // Print what we found. 250 | if (ctx.verbose) 251 | { 252 | printfln(" - Found %s%d%s modified resource(s)", 253 | ctx.color.boldBlue, resources.length, ctx.color.reset); 254 | printfln(" - Found %s%d%s pending task(s)", 255 | ctx.color.boldBlue, tasks.length, ctx.color.reset); 256 | 257 | println(ctx.color.status, ":: Building...", ctx.color.reset); 258 | } 259 | 260 | auto subgraph = ctx.state.buildGraph(resources, tasks); 261 | subgraph.build(ctx); 262 | 263 | if (ctx.verbose) 264 | println(ctx.color.status, ":: ", ctx.color.success, "Build succeeded", 265 | ctx.color.reset); 266 | } 267 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /source/button/cli/graph.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Generates input for GraphViz. 8 | */ 9 | module button.cli.graph; 10 | 11 | import button.cli.options : GraphOptions, GlobalOptions; 12 | 13 | import io.text, 14 | io.file; 15 | 16 | import io.stream : isSink; 17 | 18 | import button.resource, 19 | button.task, 20 | button.edgedata, 21 | button.graph, 22 | button.state, 23 | button.build, 24 | button.exceptions; 25 | 26 | int graphCommand(GraphOptions opts, GlobalOptions globalOpts) 27 | { 28 | import std.array : array; 29 | import std.algorithm.iteration : filter; 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 | try 39 | { 40 | string path = buildDescriptionPath(opts.path); 41 | 42 | auto state = new BuildState(path.stateName); 43 | 44 | state.begin(); 45 | scope (exit) state.rollback(); 46 | 47 | if (!opts.cached) 48 | path.syncState(state, pool, true); 49 | 50 | BuildStateGraph graph = state.buildGraph(opts.edges); 51 | 52 | if (opts.changes) 53 | { 54 | // Construct the minimal subgraph based on pending vertices 55 | auto resourceRoots = state.enumerate!(Index!Resource) 56 | .filter!(v => state.degreeIn(v) == 0 && state[v].update()) 57 | .array; 58 | 59 | auto taskRoots = state.pending!Task 60 | .filter!(v => state.degreeIn(v) == 0) 61 | .array; 62 | 63 | graph = graph.subgraph(resourceRoots, taskRoots); 64 | } 65 | 66 | graph.graphviz(state, stdout, opts.full); 67 | } 68 | catch (BuildException e) 69 | { 70 | stderr.println(":: Error: ", e.msg); 71 | return 1; 72 | } 73 | 74 | return 0; 75 | } 76 | 77 | /** 78 | * Escape a label string to be consumed by GraphViz. 79 | */ 80 | private string escapeLabel(string label) pure 81 | { 82 | import std.array : appender; 83 | import std.exception : assumeUnique; 84 | 85 | auto result = appender!(char[]); 86 | 87 | foreach (c; label) 88 | { 89 | if (c == '\\' || c == '"') 90 | result.put('\\'); 91 | result.put(c); 92 | } 93 | 94 | return assumeUnique(result.data); 95 | } 96 | 97 | unittest 98 | { 99 | assert(escapeLabel(`gcc -c "foo.c"`) == `gcc -c \"foo.c\"`); 100 | } 101 | 102 | /** 103 | * Generates input suitable for GraphViz. 104 | */ 105 | void graphviz(Stream)( 106 | BuildStateGraph graph, 107 | BuildState state, 108 | Stream stream, 109 | bool full 110 | ) 111 | if (isSink!Stream) 112 | { 113 | import io.text; 114 | import std.range : enumerate; 115 | 116 | alias A = Index!Resource; 117 | alias B = Index!Task; 118 | 119 | stream.println("digraph G {"); 120 | scope (success) stream.println("}"); 121 | 122 | // Vertices 123 | stream.println(" subgraph {\n" ~ 124 | " node [shape=ellipse, fillcolor=lightskyblue2, style=filled];" 125 | ); 126 | foreach (id; graph.vertices!A) 127 | { 128 | immutable v = state[id]; 129 | immutable name = full ? v.toString : v.toShortString; 130 | stream.printfln(` "r:%s" [label="%s", tooltip="%s"];`, id, 131 | name.escapeLabel, v.toString.escapeLabel); 132 | } 133 | stream.println(" }"); 134 | 135 | stream.println(" subgraph {\n" ~ 136 | " node [shape=box, fillcolor=gray91, style=filled];" 137 | ); 138 | foreach (id; graph.vertices!B) 139 | { 140 | immutable v = state[id]; 141 | immutable name = full ? v.toPrettyString : v.toPrettyShortString; 142 | stream.printfln(` "t:%s" [label="%s", tooltip="%s"];`, id, 143 | name.escapeLabel, v.toPrettyString.escapeLabel); 144 | } 145 | stream.println(" }"); 146 | 147 | // Cluster cycles, if any 148 | foreach (i, scc; enumerate(graph.cycles)) 149 | { 150 | stream.printfln(" subgraph cluster_%d {", i++); 151 | 152 | foreach (v; scc.vertices!A) 153 | stream.printfln(` "r:%s";`, v); 154 | 155 | foreach (v; scc.vertices!B) 156 | stream.printfln(` "t:%s";`, v); 157 | 158 | stream.println(" }"); 159 | } 160 | 161 | // Edge style, indexed by EdgeType. 162 | static immutable styles = [ 163 | "invis", // Should never get indexed 164 | "solid", // Explicit 165 | "dashed", // Implicit 166 | "bold", // Both explicit and implicit 167 | ]; 168 | 169 | // Edges 170 | foreach (edge; graph.edges!(A, B)) 171 | { 172 | stream.printfln(` "r:%s" -> "t:%s" [style=%s];`, 173 | edge.from, edge.to, styles[edge.data]); 174 | } 175 | 176 | foreach (edge; graph.edges!(B, A)) 177 | { 178 | stream.printfln(` "t:%s" -> "r:%s" [style=%s];`, 179 | edge.from, edge.to, styles[edge.data]); 180 | } 181 | } 182 | 183 | /// Ditto 184 | void graphviz(Stream)(Graph!(Resource, Task) graph, Stream stream) 185 | if (isSink!Stream) 186 | { 187 | import io.text; 188 | import std.range : enumerate; 189 | 190 | alias A = Resource; 191 | alias B = Task; 192 | 193 | stream.println("digraph G {"); 194 | scope (success) stream.println("}"); 195 | 196 | // Vertices 197 | stream.println(" subgraph {\n" ~ 198 | " node [shape=ellipse, fillcolor=lightskyblue2, style=filled];" 199 | ); 200 | foreach (v; graph.vertices!Resource) 201 | { 202 | stream.printfln(` "r:%s"`, v.escapeLabel); 203 | } 204 | stream.println(" }"); 205 | 206 | stream.println(" subgraph {\n" ~ 207 | " node [shape=box, fillcolor=gray91, style=filled];" 208 | ); 209 | foreach (v; graph.vertices!Task) 210 | { 211 | stream.printfln(` "t:%s"`, v.escapeLabel); 212 | } 213 | stream.println(" }"); 214 | 215 | // Cluster cycles, if any 216 | foreach (i, scc; enumerate(graph.cycles)) 217 | { 218 | stream.printfln(" subgraph cluster_%d {", i++); 219 | 220 | foreach (v; scc.vertices!Resource) 221 | stream.printfln(` "r:%s";`, v.escapeLabel); 222 | 223 | foreach (v; scc.vertices!Task) 224 | stream.printfln(` "t:%s";`, v.escapeLabel); 225 | 226 | stream.println(" }"); 227 | } 228 | 229 | // Edges 230 | // TODO: Style as dashed edge if implicit edge 231 | foreach (edge; graph.edges!(Resource, Task)) 232 | stream.printfln(` "r:%s" -> "t:%s";`, 233 | edge.from.escapeLabel, edge.to.escapeLabel); 234 | 235 | foreach (edge; graph.edges!(Task, Resource)) 236 | stream.printfln(` "t:%s" -> "r:%s";`, 237 | edge.from.escapeLabel, edge.to.escapeLabel); 238 | } 239 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /source/button/cli/options.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * All command line interface options. 8 | */ 9 | module button.cli.options; 10 | 11 | import std.meta : AliasSeq; 12 | 13 | import darg; 14 | 15 | import button.exceptions; 16 | 17 | struct Command 18 | { 19 | string name; 20 | } 21 | 22 | struct Description 23 | { 24 | string description; 25 | } 26 | 27 | struct GlobalOptions 28 | { 29 | @Option("help") 30 | @Help("Prints help on command line usage.") 31 | OptionFlag help; 32 | 33 | @Option("version") 34 | @Help("Prints version information.") 35 | OptionFlag version_; 36 | 37 | @Argument("command", Multiplicity.optional) 38 | string command; 39 | 40 | @Argument("args", Multiplicity.zeroOrMore) 41 | const(string)[] args; 42 | } 43 | 44 | // Generate usage and help strings at compile-time. 45 | immutable globalUsage = usageString!GlobalOptions("button"); 46 | immutable globalHelp = helpString!GlobalOptions(); 47 | 48 | @Command("help") 49 | @Description("Displays help on a given command.") 50 | struct HelpOptions 51 | { 52 | @Argument("command", Multiplicity.optional) 53 | @Help("Command to get help on.") 54 | string command; 55 | } 56 | 57 | @Command("version") 58 | @Description("Prints the current version of the program.") 59 | struct VersionOptions 60 | { 61 | } 62 | 63 | @Command("build") 64 | @Description("Runs a build.") 65 | struct BuildOptions 66 | { 67 | @Option("file", "f") 68 | @Help("Path to the build description.") 69 | string path; 70 | 71 | @Option("dryrun", "n") 72 | @Help("Don't make any functional changes. Just print what might happen.") 73 | OptionFlag dryRun; 74 | 75 | @Option("threads", "j") 76 | @Help("The number of threads to use. Default is the number of logical 77 | cores.") 78 | @MetaVar("N") 79 | size_t threads; 80 | 81 | @Option("color") 82 | @Help("When to colorize the output.") 83 | @MetaVar("{auto,never,always}") 84 | string color = "auto"; 85 | 86 | @Option("verbose", "v") 87 | @Help("Display additional information such as how long each task took to"~ 88 | " complete.") 89 | OptionFlag verbose; 90 | 91 | @Option("autopilot") 92 | @Help("After building, continue watching for changes to inputs and"~ 93 | " building again as necessary.") 94 | OptionFlag autopilot; 95 | 96 | @Option("watchdir") 97 | @Help("Used with `--autopilot`. Directory to watch for changes in. Since"~ 98 | " FUSE does not work with inotify, this is useful to use when"~ 99 | " building in a union file system.") 100 | string watchDir = "."; 101 | 102 | @Option("delay") 103 | @Help("Used with `--autopilot`. The number of milliseconds to wait for"~ 104 | " additional changes after receiving a change event before starting"~ 105 | " a build.") 106 | size_t delay = 50; 107 | } 108 | 109 | @Command("graph") 110 | @Description("Generates a graph for input into GraphViz.") 111 | struct GraphOptions 112 | { 113 | import button.edgedata : EdgeType; 114 | 115 | @Option("file", "f") 116 | @Help("Path to the build description.") 117 | string path; 118 | 119 | @Option("changes", "C") 120 | @Help("Only display the subgraph that will be traversed on an update.") 121 | OptionFlag changes; 122 | 123 | @Option("cached") 124 | @Help("Display the cached graph from the previous build.") 125 | OptionFlag cached; 126 | 127 | @Option("full") 128 | @Help("Display the full name of each vertex.") 129 | OptionFlag full; 130 | 131 | @Option("edges", "e") 132 | @MetaVar("{explicit,implicit,both}") 133 | @Help("Type of edges to show.") 134 | EdgeType edges = EdgeType.explicit; 135 | 136 | @Option("threads", "j") 137 | @Help("The number of threads to use. Default is the number of logical 138 | cores.") 139 | @MetaVar("N") 140 | size_t threads; 141 | } 142 | 143 | @Command("status") 144 | @Description("Prints the status of the build. That is, which files have been 145 | modified and which tasks are pending.") 146 | struct StatusOptions 147 | { 148 | @Option("file", "f") 149 | @Help("Path to the build description.") 150 | string path; 151 | 152 | @Option("cached") 153 | @Help("Display the cached graph from the previous build.") 154 | OptionFlag cached; 155 | 156 | @Option("color") 157 | @Help("When to colorize the output.") 158 | @MetaVar("{auto,never,always}") 159 | string color = "auto"; 160 | 161 | @Option("threads", "j") 162 | @Help("The number of threads to use. Default is the number of logical 163 | cores.") 164 | @MetaVar("N") 165 | size_t threads; 166 | } 167 | 168 | @Command("clean") 169 | @Description("Deletes all build outputs.") 170 | struct CleanOptions 171 | { 172 | @Option("file", "f") 173 | @Help("Path to the build description.") 174 | string path; 175 | 176 | @Option("dryrun", "n") 177 | @Help("Don't make any functional changes. Just print what might happen.") 178 | OptionFlag dryRun; 179 | 180 | @Option("threads", "j") 181 | @Help("The number of threads to use. Default is the number of logical 182 | cores.") 183 | @MetaVar("N") 184 | size_t threads; 185 | 186 | @Option("color") 187 | @Help("When to colorize the output.") 188 | @MetaVar("{auto,never,always}") 189 | string color = "auto"; 190 | 191 | @Option("purge") 192 | @Help("Delete the build state too.") 193 | OptionFlag purge; 194 | } 195 | 196 | @Command("init") 197 | @Description("Initializes a directory with an initial build description.") 198 | struct InitOptions 199 | { 200 | @Argument("dir", Multiplicity.optional) 201 | @Help("Directory to initialize") 202 | string dir = "."; 203 | } 204 | 205 | enum ConvertFormat 206 | { 207 | bash, 208 | } 209 | 210 | @Command("convert") 211 | @Description("Converts the build description to another format for other build systems.") 212 | struct ConvertOptions 213 | { 214 | @Option("file", "f") 215 | @Help("Path to the build description.") 216 | string path; 217 | 218 | @Option("format") 219 | @Help("Format of build description to convert to. Default is 'bash'.") 220 | @MetaVar("{bash}") 221 | ConvertFormat type; 222 | 223 | @Argument("output") 224 | @Help("Path to the output file.") 225 | @MetaVar("FILE") 226 | string output; 227 | } 228 | 229 | @Command("gc") 230 | @Description("EXPERIMENTAL") 231 | struct GCOptions 232 | { 233 | @Option("file", "f") 234 | @Help("Path to the build description.") 235 | string path; 236 | 237 | @Option("dryrun", "n") 238 | @Help("Don't make any functional changes. Just print what might happen.") 239 | OptionFlag dryRun; 240 | 241 | @Option("color") 242 | @Help("When to colorize the output.") 243 | @MetaVar("{auto,never,always}") 244 | string color = "auto"; 245 | } 246 | 247 | /** 248 | * List of all options structs. 249 | */ 250 | alias OptionsList = AliasSeq!( 251 | HelpOptions, 252 | VersionOptions, 253 | BuildOptions, 254 | GraphOptions, 255 | StatusOptions, 256 | CleanOptions, 257 | InitOptions, 258 | GCOptions, 259 | ConvertOptions, 260 | ); 261 | 262 | /** 263 | * Using the list of command functions, runs a command from the specified 264 | * string. 265 | * 266 | * Throws: InvalidCommand if the given command name is not valid. 267 | */ 268 | int runCommand(Funcs...)(string name, GlobalOptions opts) 269 | { 270 | import std.traits : Parameters, getUDAs; 271 | import std.format : format; 272 | 273 | foreach (F; Funcs) 274 | { 275 | alias Options = Parameters!F[0]; 276 | 277 | alias Commands = getUDAs!(Options, Command); 278 | 279 | foreach (C; Commands) 280 | { 281 | if (C.name == name) 282 | return F(parseArgs!Options(opts.args), opts); 283 | } 284 | } 285 | 286 | throw new InvalidCommand("button: '%s' is not a valid command. See 'button help'." 287 | .format(name)); 288 | } 289 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/command.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.command; 7 | 8 | import button.exceptions; 9 | import button.resource; 10 | import button.context; 11 | 12 | /** 13 | * Escapes the argument according to the rules of bash, the most commonly used 14 | * shell. This is mostly used for cosmetic purposes when printing out argument 15 | * arrays where they could be copy-pasted into a shell. 16 | */ 17 | string escapeShellArg(string arg) pure 18 | { 19 | import std.array : appender; 20 | import std.algorithm.searching : findAmong; 21 | import std.range : empty; 22 | import std.exception : assumeUnique; 23 | 24 | if (arg.empty) 25 | return `""`; 26 | 27 | // Characters that require the string to be quoted. 28 | static immutable special = " '~*[]?"; 29 | 30 | immutable quoted = !arg.findAmong(special).empty; 31 | 32 | auto result = appender!(char[]); 33 | 34 | if (quoted) 35 | result.put('"'); 36 | 37 | foreach (c; arg) 38 | { 39 | // Characters to escape 40 | if (c == '\\' || c == '"' || c == '$' || c == '`') 41 | { 42 | result.put("\\"); 43 | result.put(c); 44 | } 45 | else 46 | { 47 | result.put(c); 48 | } 49 | } 50 | 51 | if (quoted) 52 | result.put('"'); 53 | 54 | return assumeUnique(result.data); 55 | } 56 | 57 | unittest 58 | { 59 | assert(escapeShellArg(``) == `""`); 60 | assert(escapeShellArg(`foo`) == `foo`); 61 | assert(escapeShellArg(`foo bar`) == `"foo bar"`); 62 | assert(escapeShellArg(`foo'bar`) == `"foo'bar"`); 63 | assert(escapeShellArg(`foo?bar`) == `"foo?bar"`); 64 | assert(escapeShellArg(`foo*.c`) == `"foo*.c"`); 65 | assert(escapeShellArg(`foo.[ch]`) == `"foo.[ch]"`); 66 | assert(escapeShellArg(`~foobar`) == `"~foobar"`); 67 | assert(escapeShellArg(`$PATH`) == `\$PATH`); 68 | assert(escapeShellArg(`\`) == `\\`); 69 | assert(escapeShellArg(`foo"bar"`) == `foo\"bar\"`); 70 | assert(escapeShellArg("`pwd`") == "\\`pwd\\`"); 71 | } 72 | 73 | /** 74 | * A single command. 75 | */ 76 | struct Command 77 | { 78 | /** 79 | * Arguments to execute. The first argument is the name of the executable. 80 | */ 81 | immutable(string)[] args; 82 | 83 | alias args this; 84 | 85 | // Root of the build directory. This is used to normalize implicit resource 86 | // paths. 87 | string buildRoot; 88 | 89 | /** 90 | * The result of executing a command. 91 | */ 92 | struct Result 93 | { 94 | import core.time : Duration; 95 | 96 | /** 97 | * Implicit input and output resources this command used. 98 | */ 99 | Resource[] inputs, outputs; 100 | 101 | /** 102 | * How long it took the command to run from start to finish. 103 | */ 104 | Duration duration; 105 | } 106 | 107 | this(immutable(string)[] args) 108 | { 109 | assert(args.length > 0, "A command must have >0 arguments"); 110 | 111 | this.args = args; 112 | } 113 | 114 | /** 115 | * Compares this command with another. 116 | */ 117 | int opCmp()(const auto ref typeof(this) that) const pure nothrow 118 | { 119 | import std.algorithm.comparison : cmp; 120 | return cmp(this.args, that.args); 121 | } 122 | 123 | /// Ditto 124 | bool opEquals()(const auto ref typeof(this) that) const pure nothrow 125 | { 126 | return this.opCmp(that) == 0; 127 | } 128 | 129 | unittest 130 | { 131 | import std.algorithm.comparison : cmp; 132 | 133 | static assert(Command(["a", "b"]) == Command(["a", "b"])); 134 | static assert(Command(["a", "b"]) != Command(["a", "c"])); 135 | static assert(Command(["a", "b"]) < Command(["a", "c"])); 136 | static assert(Command(["b", "a"]) > Command(["a", "b"])); 137 | 138 | static assert(cmp([Command(["a", "b"])], [Command(["a", "b"])]) == 0); 139 | static assert(cmp([Command(["a", "b"])], [Command(["a", "c"])]) < 0); 140 | static assert(cmp([Command(["a", "c"])], [Command(["a", "b"])]) > 0); 141 | } 142 | 143 | /** 144 | * Returns a string representation of the command. 145 | * 146 | * Since the command is in argv format, we format it into a string as one 147 | * would enter into a shell. 148 | */ 149 | string toPrettyString() const pure 150 | { 151 | import std.array : join; 152 | import std.algorithm.iteration : map; 153 | 154 | return args.map!(arg => arg.escapeShellArg).join(" "); 155 | } 156 | 157 | /** 158 | * Returns a short string representation of the command. 159 | */ 160 | @property string toPrettyShortString() const pure nothrow 161 | { 162 | return args[0]; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/handlers/base.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Delegates dependency detection to the child process. 8 | * 9 | * This is done by creating pipes for the child process to send back the 10 | * dependency information. The environment variables BUTTON_INPUTS and 11 | * BUTTON_OUTPUTS are set to the file descriptors that the child should write 12 | * to. This is also useful for the child process to determine if it is running 13 | * under this build system or not. The child only needs to check if both of 14 | * those environment variables are set. 15 | * 16 | * This handler should be used for commands that know how to communicate with 17 | * Button. It is also commonly used by other handlers to run the command. 18 | */ 19 | module button.handlers.base; 20 | 21 | import button.events; 22 | import button.resource; 23 | import button.context; 24 | 25 | // Open /dev/null to be used by all child processes as its standard input. 26 | version (Posix) 27 | { 28 | private __gshared static int devnull; 29 | 30 | shared static this() 31 | { 32 | import io.file.stream : sysEnforce; 33 | import core.sys.posix.fcntl : open, O_RDONLY; 34 | devnull = open("/dev/null", O_RDONLY); 35 | sysEnforce(devnull != -1, "Failed to open /dev/null"); 36 | } 37 | 38 | shared static ~this() 39 | { 40 | import core.sys.posix.unistd : close; 41 | close(devnull); 42 | } 43 | } 44 | 45 | version (Posix) 46 | void execute( 47 | ref BuildContext ctx, 48 | const(string)[] args, 49 | string workDir, 50 | ref Resources inputs, 51 | ref Resources outputs 52 | ) 53 | { 54 | // FIXME: Commands should use a separate logger. It only uses the 55 | // Events interface because there used to never be more than one command in 56 | // a task. 57 | 58 | import core.sys.posix.unistd; 59 | import core.stdc.stdio : sprintf; 60 | 61 | import io.file.stream : sysEnforce; 62 | 63 | import std.string : toStringz; 64 | import std.array : array; 65 | 66 | import button.deps : deps; 67 | import button.exceptions : CommandError; 68 | 69 | int[2] stdfds, inputfds, outputfds; 70 | 71 | sysEnforce(pipe(stdfds) != -1); // Standard output 72 | sysEnforce(pipe(inputfds) != -1); // Implicit inputs 73 | sysEnforce(pipe(outputfds) != -1); // Implicit outputs 74 | 75 | // Convert D command argument list to a null-terminated argument list 76 | auto argv = new const(char)*[args.length+1]; 77 | foreach (i; 0 .. args.length) 78 | argv[i] = toStringz(args[i]); 79 | argv[$-1] = null; 80 | 81 | // Working directory 82 | const(char)* cwd = null; 83 | if (workDir.length) 84 | cwd = workDir.toStringz(); 85 | 86 | char[16] inputsenv, outputsenv; 87 | sprintf(inputsenv.ptr, "%d", inputfds[1]); 88 | sprintf(outputsenv.ptr, "%d", outputfds[1]); 89 | 90 | immutable pid = fork(); 91 | sysEnforce(pid >= 0, "Failed to fork current process"); 92 | 93 | // Child process 94 | if (pid == 0) 95 | { 96 | close(stdfds[0]); 97 | close(inputfds[0]); 98 | close(outputfds[0]); 99 | 100 | executeChild(argv, cwd, devnull, stdfds[1], inputfds[1], 101 | outputfds[1], inputsenv.ptr, outputsenv.ptr); 102 | 103 | // Unreachable 104 | } 105 | 106 | // In the parent process 107 | close(stdfds[1]); 108 | close(inputfds[1]); 109 | close(outputfds[1]); 110 | 111 | immutable worker = ctx.pool.workerIndex; 112 | 113 | // TODO: Parse the resources as they come in instead of all at once at 114 | // the end. 115 | auto implicit = readOutput(stdfds[0], inputfds[0], outputfds[0], worker, 116 | ctx.events); 117 | 118 | // Add the inputs and outputs 119 | inputs.put(implicit.inputs.deps); 120 | outputs.put(implicit.outputs.deps); 121 | 122 | // Wait for the child to exit 123 | immutable exitCode = waitFor(pid); 124 | 125 | if (exitCode != 0) 126 | throw new CommandError(exitCode); 127 | } 128 | 129 | private version (Posix) 130 | { 131 | import std.array : Appender; 132 | 133 | auto readOutput(int stdfd, int inputsfd, int outputsfd, size_t worker, Events events) 134 | { 135 | import std.array : appender; 136 | import std.algorithm : max; 137 | import std.typecons : tuple; 138 | import std.exception : assumeUnique; 139 | 140 | import core.stdc.errno; 141 | import core.sys.posix.unistd; 142 | import core.sys.posix.sys.select; 143 | 144 | import io.file.stream : SysException; 145 | 146 | ubyte[4096] buf; 147 | fd_set readfds = void; 148 | 149 | auto inputs = appender!(ubyte[]); 150 | auto outputs = appender!(ubyte[]); 151 | 152 | while (true) 153 | { 154 | FD_ZERO(&readfds); 155 | 156 | int nfds = 0; 157 | 158 | if (stdfd != -1) 159 | { 160 | FD_SET(stdfd, &readfds); 161 | nfds = max(nfds, stdfd); 162 | } 163 | 164 | if (inputsfd != -1) 165 | { 166 | FD_SET(inputsfd, &readfds); 167 | nfds = max(nfds, inputsfd); 168 | } 169 | 170 | if (outputsfd != -1) 171 | { 172 | FD_SET(outputsfd, &readfds); 173 | nfds = max(nfds, outputsfd); 174 | } 175 | 176 | if (nfds == 0) 177 | break; 178 | 179 | immutable r = select(nfds + 1, &readfds, null, null, null); 180 | 181 | if (r == -1) 182 | { 183 | if (errno == EINTR) 184 | continue; 185 | 186 | throw new SysException("select() failed"); 187 | } 188 | 189 | if (r == 0) break; // Nothing in the set 190 | 191 | // Read stdout/stderr from child 192 | if (FD_ISSET(stdfd, &readfds)) 193 | { 194 | immutable len = read(stdfd, buf.ptr, buf.length); 195 | if (len > 0) 196 | { 197 | events.taskOutput(worker, buf[0 .. len]); 198 | } 199 | else 200 | { 201 | close(stdfd); 202 | stdfd = -1; 203 | } 204 | } 205 | 206 | // Read inputs from child 207 | if (FD_ISSET(inputsfd, &readfds)) 208 | readFromChild(inputsfd, inputs, buf); 209 | 210 | // Read inputs from child 211 | if (FD_ISSET(outputsfd, &readfds)) 212 | readFromChild(outputsfd, outputs, buf); 213 | } 214 | 215 | return tuple!("inputs", "outputs")( 216 | assumeUnique(inputs.data), 217 | assumeUnique(outputs.data) 218 | ); 219 | } 220 | 221 | void readFromChild(ref int fd, ref Appender!(ubyte[]) a, ubyte[] buf) 222 | { 223 | import core.sys.posix.unistd : read, close; 224 | 225 | immutable len = read(fd, buf.ptr, buf.length); 226 | 227 | if (len > 0) 228 | { 229 | a.put(buf[0 .. len]); 230 | } 231 | else 232 | { 233 | // Either the other end of the pipe was closed or the end of the 234 | // stream was reached. 235 | close(fd); 236 | fd = -1; 237 | } 238 | } 239 | 240 | int waitFor(int pid) 241 | { 242 | import core.sys.posix.sys.wait; 243 | import core.stdc.errno; 244 | import io.file.stream : SysException; 245 | 246 | while (true) 247 | { 248 | int status; 249 | immutable check = waitpid(pid, &status, 0) == -1; 250 | if (check == -1) 251 | { 252 | if (errno == ECHILD) 253 | { 254 | throw new SysException("Child process does not exist"); 255 | } 256 | else 257 | { 258 | // Keep waiting 259 | assert(errno == EINTR); 260 | continue; 261 | } 262 | } 263 | 264 | if (WIFEXITED(status)) 265 | return WEXITSTATUS(status); 266 | else if (WIFSIGNALED(status)) 267 | return -WTERMSIG(status); 268 | } 269 | } 270 | 271 | /** 272 | * Executes the child process. This is called after the fork(). 273 | * 274 | * NOTE: Memory should not be allocated here. It can cause the child process 275 | * to hang. 276 | */ 277 | void executeChild(const(char*)[] argv, const(char)* cwd, 278 | int devnull, int stdfd, 279 | int inputsfd, int outputsfd, 280 | const(char)* inputsenv, const(char)* outputsenv) 281 | { 282 | import core.sys.posix.unistd; 283 | import core.sys.posix.stdlib : setenv; 284 | import core.stdc.stdio : perror, stderr, fprintf; 285 | import core.stdc.string : strerror; 286 | import core.stdc.errno : errno; 287 | 288 | // Get standard input from /dev/null. With potentially multiple tasks 289 | // executing in parallel, the child cannot use standard input. 290 | if (dup2(devnull, STDIN_FILENO) == -1) 291 | { 292 | perror("dup2"); 293 | _exit(1); 294 | } 295 | 296 | close(devnull); 297 | 298 | // Let the child know two bits of information: (1) that it is being run 299 | // under this build system and (2) which file descriptors to use to send 300 | // back dependency information. 301 | setenv("BUTTON_INPUTS", inputsenv, 1); 302 | setenv("BUTTON_OUTPUTS", outputsenv, 1); 303 | 304 | // Redirect stdout/stderr to the pipe the parent reads from. There is no 305 | // differentiation between stdout and stderr. 306 | if (dup2(stdfd, STDOUT_FILENO) == -1) 307 | { 308 | perror("dup2"); 309 | _exit(1); 310 | } 311 | 312 | if (dup2(stdfd, STDERR_FILENO) == -1) 313 | { 314 | perror("dup2"); 315 | _exit(1); 316 | } 317 | 318 | close(stdfd); 319 | 320 | if (cwd && (chdir(cwd) != 0)) 321 | { 322 | fprintf(stderr, "button: Error: Invalid working directory '%s' (%s)\n", 323 | cwd, strerror(errno)); 324 | _exit(1); 325 | } 326 | 327 | execvp(argv[0], argv.ptr); 328 | 329 | // If we get this far, something went wrong. Most likely, the command does 330 | // not exist. 331 | fprintf(stderr, "button: Failed executing process '%s' (%s)\n", 332 | argv[0], strerror(errno)); 333 | _exit(1); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /source/button/handlers/dmd.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles running "dmd" processes. 8 | * 9 | * Inputs and outputs are detected by parsing and modifying the command line 10 | * before execution. DMD has a "-deps" option for writing inputs to a file. This 11 | * option is dynamically added to the command line and parsed after the process 12 | * exits. 13 | */ 14 | module button.handlers.dmd; 15 | 16 | import button.resource; 17 | import button.context; 18 | 19 | import std.path; 20 | import io.file; 21 | 22 | private struct Options 23 | { 24 | // Flags 25 | bool compileFlag; // -c 26 | bool coverageFlag; // -cov 27 | bool libFlag; // -lib 28 | bool sharedFlag; // -shared 29 | bool docFlag; // -D 30 | bool headerFlag; // -H 31 | bool mapFlag; // -map 32 | bool suppressObjectsFlag; // -o- 33 | bool jsonFlag; // -X 34 | bool opFlag; // -op 35 | 36 | // Options with arguments 37 | string outputDir; // -od 38 | string outputFile; // -of 39 | string depsFile; // -deps= 40 | string docDir; // -Dd 41 | string docFile; // -Df 42 | string headerDir; // -Hd 43 | string headerFile; // -Hf 44 | string[] importDirs; // -I 45 | string[] stringImportDirs; // -J 46 | string[] linkerFlags; // -L 47 | const(string)[] run; // -run 48 | string jsonFile; // -Xf 49 | string cmdFile; // @cmdfile 50 | 51 | // Left over files on the command line 52 | string[] files; 53 | 54 | /** 55 | * Returns the object file path for the given source file path. 56 | */ 57 | string objectPath(string sourceFile) const pure 58 | { 59 | if (!opFlag) 60 | sourceFile = baseName(sourceFile); 61 | return buildPath(outputDir, setExtension(sourceFile, ".o")); 62 | } 63 | 64 | /** 65 | * Returns a list of object file paths. 66 | */ 67 | const(char[])[] objects() const pure 68 | { 69 | import std.algorithm.iteration : map, filter; 70 | import std.algorithm.searching : endsWith; 71 | import std.array : array; 72 | 73 | if (suppressObjectsFlag) 74 | return []; 75 | 76 | // If -c is specified, all source files are compiled into separate 77 | // object files. If -c is not specified, all sources files are compiled 78 | // into a single object file which is named based on the first source 79 | // file specified. 80 | if (compileFlag) 81 | { 82 | if (outputFile) 83 | return [outputFile]; 84 | else 85 | return files 86 | .filter!(p => p.endsWith(".d")) 87 | .map!(p => objectPath(p)) 88 | .array(); 89 | } 90 | 91 | // Object name is based on -of 92 | if (outputFile) 93 | return [objectPath(outputFile)]; 94 | 95 | auto dSources = files.filter!(p => p.endsWith(".d")); 96 | if (dSources.empty) 97 | return []; 98 | 99 | return [objectPath(dSources.front)]; 100 | } 101 | 102 | /** 103 | * Returns the static library file path. 104 | */ 105 | string staticLibraryPath() const pure 106 | { 107 | import std.algorithm.iteration : filter; 108 | import std.algorithm.searching : endsWith; 109 | 110 | // If the output file has no extension, ".a" is appended. 111 | 112 | // Note that -op and -o- have no effect when building static libraries. 113 | 114 | string path; 115 | 116 | if (outputFile) 117 | path = defaultExtension(outputFile, ".a"); 118 | else 119 | { 120 | // If no output file is specified with -of, the output file is based on 121 | // the name of the first source file. 122 | auto dSources = files.filter!(p => p.endsWith(".d")); 123 | if (dSources.empty) 124 | return null; 125 | 126 | path = setExtension(baseName(dSources.front), ".a"); 127 | } 128 | 129 | return buildPath(outputDir, path); 130 | } 131 | 132 | /** 133 | * Returns the shared library file path. 134 | */ 135 | string sharedLibraryPath() const pure 136 | { 137 | import std.algorithm.iteration : filter; 138 | import std.algorithm.searching : endsWith; 139 | 140 | if (outputFile) 141 | return outputFile; 142 | 143 | // If no output file is specified with -of, the output file is based on 144 | // the name of the first source file. 145 | auto dSources = files.filter!(p => p.endsWith(".d")); 146 | if (dSources.empty) 147 | return null; 148 | 149 | return setExtension(baseName(dSources.front), ".so"); 150 | } 151 | 152 | /** 153 | * Returns the static library file path. 154 | */ 155 | string executablePath() const pure 156 | { 157 | import std.algorithm.iteration : filter; 158 | import std.algorithm.searching : endsWith; 159 | 160 | if (outputFile) 161 | return outputFile; 162 | 163 | // If no output file is specified with -of, the output file is based on 164 | // the name of the first source file. 165 | auto dSources = files.filter!(p => p.endsWith(".d")); 166 | if (dSources.empty) 167 | return null; 168 | 169 | return stripExtension(baseName(dSources.front)); 170 | } 171 | } 172 | 173 | /** 174 | * Parses DMD arguments. 175 | */ 176 | private Options parseArgs(const(string)[] args) pure 177 | { 178 | import std.algorithm.searching : startsWith; 179 | import std.exception : enforce; 180 | import std.range : front, popFront, empty; 181 | 182 | Options opts; 183 | 184 | while (!args.empty) 185 | { 186 | string arg = args.front; 187 | 188 | if (arg == "-c") 189 | opts.compileFlag = true; 190 | else if (arg == "-cov") 191 | opts.coverageFlag = true; 192 | else if (arg == "-lib") 193 | opts.libFlag = true; 194 | else if (arg == "-shared") 195 | opts.sharedFlag = true; 196 | else if (arg == "-lib") 197 | opts.docFlag = true; 198 | else if (arg == "-H") 199 | opts.headerFlag = true; 200 | else if (arg == "-map") 201 | opts.mapFlag = true; 202 | else if (arg == "-X") 203 | opts.jsonFlag = true; 204 | else if (arg == "-op") 205 | opts.opFlag = true; 206 | else if (arg == "-o-") 207 | opts.suppressObjectsFlag = true; 208 | else if (arg == "-run") 209 | { 210 | args.popFront(); 211 | opts.run = args; 212 | break; 213 | } 214 | else if (arg.startsWith("-deps=")) 215 | opts.depsFile = arg["-deps=".length .. $]; 216 | else if (arg.startsWith("-od")) 217 | opts.outputDir = arg["-od".length .. $]; 218 | else if (arg.startsWith("-of")) 219 | opts.outputFile = arg["-of".length .. $]; 220 | else if (arg.startsWith("-Xf")) 221 | opts.jsonFile = arg["-Xf".length .. $]; 222 | else if (arg.startsWith("-Dd")) 223 | opts.docDir = arg["-Dd".length .. $]; 224 | else if (arg.startsWith("-Df")) 225 | opts.docFile = arg["-Df".length .. $]; 226 | else if (arg.startsWith("-Hd")) 227 | opts.headerDir = arg["-Hd".length .. $]; 228 | else if (arg.startsWith("-Hf")) 229 | opts.headerFile = arg["-Hf".length .. $]; 230 | else if (arg.startsWith("-I")) 231 | opts.importDirs ~= arg["-I".length .. $]; 232 | else if (arg.startsWith("-J")) 233 | opts.stringImportDirs ~= arg["-J".length .. $]; 234 | else if (arg.startsWith("-L")) 235 | opts.stringImportDirs ~= arg["-L".length .. $]; 236 | else if (arg.startsWith("@")) 237 | opts.cmdFile = arg["@".length .. $]; 238 | else if (!arg.startsWith("-")) 239 | opts.files ~= arg; 240 | 241 | args.popFront(); 242 | } 243 | 244 | return opts; 245 | } 246 | 247 | void execute( 248 | ref BuildContext ctx, 249 | const(string)[] args, 250 | string workDir, 251 | ref Resources inputs, 252 | ref Resources outputs 253 | ) 254 | { 255 | import button.handlers.base : base = execute; 256 | 257 | import std.algorithm.iteration : map, filter, uniq; 258 | import std.algorithm.searching : endsWith; 259 | import std.range : enumerate, empty, popFront, front; 260 | import std.regex : regex, matchAll; 261 | import std.file : remove; 262 | import std.array : array; 263 | 264 | import io.text, io.range; 265 | 266 | Options opts = parseArgs(args[1 .. $]); 267 | 268 | string depsPath; 269 | 270 | if (opts.depsFile is null) 271 | { 272 | // Output -deps to a temporary file. 273 | depsPath = tempFile(AutoDelete.no).path; 274 | args ~= "-deps=" ~ depsPath; 275 | } 276 | else 277 | { 278 | // -deps= was specified already. Just use this path to get the 279 | // dependencies. 280 | depsPath = opts.depsFile; 281 | } 282 | 283 | // Delete the temporary -deps file when done. 284 | scope (exit) if (opts.depsFile is null) remove(depsPath); 285 | 286 | base(ctx, args, workDir, inputs, outputs); 287 | 288 | // Add the inputs from the dependency file. 289 | static r = regex(`\((.*?)\)`); 290 | foreach (line; File(depsPath).byLine) 291 | foreach (c; line.matchAll(r)) 292 | inputs.put(c[1]); 293 | 294 | inputs.put(opts.files); 295 | 296 | // Determine the output files based on command line options. If no output 297 | // file name is specified with -of, the file name is based on the first 298 | // source file specified on the command line. 299 | if (opts.libFlag) 300 | { 301 | if (auto path = opts.staticLibraryPath()) 302 | outputs.put(path); 303 | } 304 | else if (opts.compileFlag) 305 | { 306 | outputs.put(opts.objects); 307 | } 308 | else if (opts.sharedFlag) 309 | { 310 | if (auto path = opts.sharedLibraryPath()) 311 | outputs.put(path); 312 | 313 | outputs.put(opts.objects); 314 | } 315 | else 316 | { 317 | if (!opts.suppressObjectsFlag) 318 | { 319 | // Binary executable. 320 | if (auto path = opts.executablePath()) 321 | outputs.put(path); 322 | 323 | // Objects 324 | outputs.put(opts.objects); 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /source/button/handlers/gcc.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles running gcc processes. 8 | * 9 | * Inputs and outputs are detected by adding the -MMD option and parsing the 10 | * deps file that gcc produces. 11 | */ 12 | module button.handlers.gcc; 13 | 14 | import button.exceptions; 15 | import button.resource; 16 | import button.context; 17 | 18 | import std.range.primitives : isInputRange, ElementEncodingType, 19 | front, empty, popFront; 20 | 21 | import std.traits : isSomeChar; 22 | 23 | /** 24 | * Helper function to escape a character. 25 | */ 26 | private C escapeChar(C)(C c) 27 | if (isSomeChar!C) 28 | { 29 | switch (c) 30 | { 31 | case 't': return '\t'; 32 | case 'v': return '\v'; 33 | case 'r': return '\r'; 34 | case 'n': return '\n'; 35 | case 'b': return '\b'; 36 | case 'f': return '\f'; 37 | case '0': return '\0'; 38 | default: return c; 39 | } 40 | } 41 | 42 | /** 43 | * A single Make rule. 44 | */ 45 | struct MakeRule 46 | { 47 | string target; 48 | string[] deps; 49 | } 50 | 51 | /** 52 | * An input range of Make rules. 53 | * 54 | * This parses a deps file that gcc produces. The file consists of simple Make 55 | * rules. Rules are separated by lines (discounting line continuations). Each 56 | * rule consists of a target file and its dependencies. 57 | */ 58 | struct MakeRules(Source) 59 | if (isInputRange!Source && isSomeChar!(ElementEncodingType!Source)) 60 | { 61 | private 62 | { 63 | import std.array : Appender; 64 | import std.traits : Unqual; 65 | 66 | alias C = Unqual!(ElementEncodingType!Source); 67 | 68 | Source source; 69 | bool _empty; 70 | MakeRule current; 71 | 72 | Appender!(C[]) buf; 73 | } 74 | 75 | this(Source source) 76 | { 77 | this.source = source; 78 | popFront(); 79 | } 80 | 81 | @property 82 | bool empty() const pure nothrow 83 | { 84 | return _empty; 85 | } 86 | 87 | @property 88 | const(MakeRule) front() const pure nothrow 89 | { 90 | return current; 91 | } 92 | 93 | /** 94 | * Parses a single file name. 95 | */ 96 | private string parseFileName() 97 | { 98 | import std.uni : isWhite; 99 | 100 | buf.clear(); 101 | 102 | while (!source.empty) 103 | { 104 | immutable c = source.front; 105 | 106 | if (c == ':' || c.isWhite) 107 | { 108 | // ':' delimits a target. 109 | break; 110 | } 111 | else if (c == '\\') 112 | { 113 | // Skip past the '\\' 114 | source.popFront(); 115 | if (source.empty) 116 | break; 117 | 118 | immutable e = source.front; 119 | if (e == '\n') 120 | { 121 | // Line continuation 122 | source.popFront(); 123 | } 124 | else 125 | { 126 | buf.put(escapeChar(e)); 127 | source.popFront(); 128 | } 129 | 130 | continue; 131 | } 132 | 133 | // Regular character 134 | buf.put(c); 135 | source.popFront(); 136 | } 137 | 138 | return buf.data.idup; 139 | } 140 | 141 | /** 142 | * Skips spaces. 143 | */ 144 | private void skipSpace() 145 | { 146 | import std.uni : isSpace; 147 | while (!source.empty && isSpace(source.front)) 148 | source.popFront(); 149 | } 150 | 151 | /** 152 | * Skips whitespace 153 | */ 154 | private void skipWhite() 155 | { 156 | import std.uni : isWhite; 157 | while (!source.empty && isWhite(source.front)) 158 | source.popFront(); 159 | } 160 | 161 | /** 162 | * Parses the list of dependencies after the target. 163 | * 164 | * Returns: The list of dependencies. 165 | */ 166 | private string[] parseDeps() 167 | { 168 | Appender!(string[]) deps; 169 | 170 | skipSpace(); 171 | 172 | while (!source.empty) 173 | { 174 | // A new line delimits the dependency list 175 | if (source.front == '\n') 176 | { 177 | source.popFront(); 178 | break; 179 | } 180 | 181 | auto dep = parseFileName(); 182 | if (dep.length) 183 | deps.put(dep); 184 | 185 | if (!source.empty && source.front == ':') 186 | throw new MakeParserError("Unexpected ':'"); 187 | 188 | skipSpace(); 189 | } 190 | 191 | return deps.data; 192 | } 193 | 194 | /** 195 | * Parses a rule. 196 | */ 197 | private MakeRule parseRule() 198 | { 199 | string target = parseFileName(); 200 | if (target.empty) 201 | throw new MakeParserError("Empty target name"); 202 | 203 | skipSpace(); 204 | 205 | if (source.empty) 206 | throw new MakeParserError("Unexpected end of file"); 207 | 208 | if (source.front != ':') 209 | throw new MakeParserError("Expected ':' after target name"); 210 | 211 | source.popFront(); 212 | 213 | // Parse dependency names 214 | auto deps = parseDeps(); 215 | 216 | skipWhite(); 217 | 218 | return MakeRule(target, deps); 219 | } 220 | 221 | void popFront() 222 | { 223 | skipWhite(); 224 | 225 | if (source.empty) 226 | { 227 | _empty = true; 228 | return; 229 | } 230 | 231 | current = parseRule(); 232 | } 233 | } 234 | 235 | /** 236 | * Convenience function for constructing a MakeRules range. 237 | */ 238 | MakeRules!Source makeRules(Source)(Source source) 239 | if (isInputRange!Source && isSomeChar!(ElementEncodingType!Source)) 240 | { 241 | return MakeRules!Source(source); 242 | } 243 | 244 | unittest 245 | { 246 | import std.array : array; 247 | import std.exception : collectException; 248 | import std.algorithm.comparison : equal; 249 | 250 | static assert(isInputRange!(MakeRules!string)); 251 | 252 | { 253 | auto rules = makeRules( 254 | "\n\nfoo.c : foo.h \\\n bar.h \\\n" 255 | ); 256 | 257 | assert(rules.equal([ 258 | MakeRule("foo.c", ["foo.h", "bar.h"]), 259 | ])); 260 | } 261 | 262 | { 263 | auto rules = makeRules( 264 | "foo.c : foo.h \\\n bar.h\n"~ 265 | " \nbar.c : bar.h\n"~ 266 | "\n \nbaz.c:\n"~ 267 | "ba\\\nz.c: blah.h\n"~ 268 | `foo\ bar: bing\ bang` 269 | ); 270 | 271 | assert(rules.equal([ 272 | MakeRule("foo.c", ["foo.h", "bar.h"]), 273 | MakeRule("bar.c", ["bar.h"]), 274 | MakeRule("baz.c", []), 275 | MakeRule("baz.c", ["blah.h"]), 276 | MakeRule("foo bar", ["bing bang"]), 277 | ])); 278 | } 279 | 280 | assert(collectException!MakeParserError(makeRules( 281 | "foo.c: foo.h: bar.h" 282 | ).array)); 283 | } 284 | 285 | void execute( 286 | ref BuildContext ctx, 287 | const(string)[] args, 288 | string workDir, 289 | ref Resources inputs, 290 | ref Resources outputs 291 | ) 292 | { 293 | import std.file : remove; 294 | 295 | import io.file : File, tempFile, AutoDelete; 296 | import io.range : byBlock; 297 | 298 | import button.handlers.base : base = execute; 299 | 300 | // Create the temporary file for the dependencies. 301 | auto depsPath = tempFile(AutoDelete.no).path; 302 | scope (exit) remove(depsPath); 303 | 304 | // Tell gcc to write dependencies to our temporary file. 305 | args ~= ["-MMD", "-MF", depsPath]; 306 | 307 | base(ctx, args, workDir, inputs, outputs); 308 | 309 | // TODO: Parse the command line arguments for -I and -o options. 310 | 311 | // Parse the dependencies 312 | auto deps = File(depsPath).byBlock!char; 313 | foreach (rule; makeRules(&deps)) 314 | { 315 | outputs.put(rule.target); 316 | inputs.put(rule.deps); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /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/handlers/recursive.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Handles running Button recursively. 8 | * 9 | * Instead of running another child process, we can use the same process to run 10 | * Button recursively. 11 | * 12 | * There are a several advantages to doing it this way: 13 | * - The same thread pool can be reused. Thus, the correct number of worker 14 | * threads is always used. 15 | * - The same verbosity settings as the parent can be used. 16 | * - The same output coloring mode can be used as the parent process. 17 | * - Logging of output is more immediate. Output is normally accumulated and 18 | * then printed all at once so it isn't interleaved with everything else. 19 | * - Avoids the overhead of running another process. However, in general, this 20 | * is a non-issue. 21 | * 22 | * The only disadvantage to doing it this way is that it is more difficult to 23 | * implement. 24 | */ 25 | module button.handlers.recursive; 26 | 27 | import std.parallelism : TaskPool; 28 | 29 | import button.resource; 30 | import button.context; 31 | import button.build; 32 | import button.state; 33 | import button.cli; 34 | 35 | import darg; 36 | 37 | void execute( 38 | ref BuildContext ctx, 39 | const(string)[] args, 40 | string workDir, 41 | ref Resources inputs, 42 | ref Resources outputs 43 | ) 44 | { 45 | import button.handlers.base : base = execute; 46 | 47 | import std.path : dirName, absolutePath, buildPath; 48 | 49 | auto globalOpts = parseArgs!GlobalOptions(args[1 .. $], Config.ignoreUnknown); 50 | 51 | // Not the build command, forward to the base handler. 52 | if (globalOpts.command != "build") 53 | { 54 | base(ctx, args, workDir, inputs, outputs); 55 | return; 56 | } 57 | 58 | auto opts = parseArgs!BuildOptions(globalOpts.args); 59 | 60 | string path; 61 | 62 | if (opts.path.length) 63 | path = buildPath(workDir, opts.path); 64 | else 65 | path = buildDescriptionPath(workDir); 66 | 67 | auto state = new BuildState(path.stateName); 68 | auto dir = path.dirName; 69 | 70 | // Reuse as much of the parent build context as possible. 71 | auto newContext = BuildContext( 72 | dir.absolutePath, 73 | ctx.pool, ctx.events, state, 74 | ctx.dryRun, ctx.verbose, ctx.color 75 | ); 76 | 77 | state.begin(); 78 | 79 | scope (exit) 80 | { 81 | if (newContext.dryRun) 82 | state.rollback(); 83 | else 84 | state.commit(); 85 | } 86 | 87 | syncBuildState(state, newContext.pool, path); 88 | 89 | // TODO: Get changes from the parent build system because this is 90 | // duplicating work that has already been done. 91 | queueChanges(state, newContext.pool, newContext.color); 92 | 93 | // Do the build. 94 | update(newContext); 95 | 96 | // Publish implicit resources to parent build 97 | foreach (v; state.enumerate!(Index!Resource)) 98 | { 99 | immutable degreeIn = state.degreeIn(v); 100 | immutable degreeOut = state.degreeOut(v); 101 | 102 | if (degreeIn == 0 && degreeOut == 0) 103 | continue; // Dangling resource 104 | 105 | auto r = state[v]; 106 | 107 | // Make sure we are reporting inputs/outputs relative to the correct 108 | // directory. 109 | r.path = buildPath(dir, r.path); 110 | 111 | if (degreeIn == 0) 112 | inputs.put(r); 113 | else 114 | outputs.put(r); 115 | } 116 | } 117 | 118 | /** 119 | * Updates the database with any changes to the build description. 120 | */ 121 | private void syncBuildState(BuildState state, TaskPool pool, string path) 122 | { 123 | auto r = state[BuildState.buildDescId]; 124 | r.path = path; 125 | if (r.update()) 126 | { 127 | path.syncState(state, pool); 128 | 129 | // Update the build description resource 130 | state[BuildState.buildDescId] = r; 131 | } 132 | } 133 | 134 | /** 135 | * Builds pending vertices. 136 | */ 137 | private void update(ref BuildContext ctx) 138 | { 139 | import std.array : array; 140 | 141 | import button.task : Task; 142 | 143 | auto resources = ctx.state.pending!Resource.array; 144 | auto tasks = ctx.state.pending!Task.array; 145 | 146 | if (resources.length == 0 && tasks.length == 0) 147 | return; 148 | 149 | ctx.state.buildGraph(resources, tasks).build(ctx); 150 | } 151 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/button/handlers/tracer/strace.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * The tracer traces system calls to determine inputs and outputs. This is very 8 | * slow and should only be used as a last resort when there are no other 9 | * suitable handlers. 10 | * 11 | * FIXME: Implement this using ptrace directly. This will eliminate the 12 | * dependency on strace, as it is not installed by default. It will also 13 | * eliminate the small amount of overhead of spawning an extra process. 14 | */ 15 | module button.handlers.tracer.strace; 16 | 17 | version (linux): 18 | 19 | import button.resource; 20 | import button.context; 21 | 22 | import io.file; 23 | 24 | private struct Trace 25 | { 26 | private 27 | { 28 | import std.regex : regex; 29 | 30 | static re_open = regex(`open\("([^"]*)", ([^,)]*)`); 31 | static re_creat = regex(`creat\("([^"]*)",`); 32 | static re_rename = regex(`rename\("([^"]*)", "([^"]*)"\)`); 33 | static re_mkdir = regex(`mkdir\("([^"]*)", (0[0-7]*)\)`); 34 | static re_chdir = regex(`chdir\("([^"]*)"\)`); 35 | } 36 | 37 | /** 38 | * Paths that start with these fragments are ignored. 39 | */ 40 | private static immutable ignoredPaths = [ 41 | "/dev/", 42 | "/etc/", 43 | "/proc/", 44 | "/tmp/", 45 | "/usr/", 46 | ]; 47 | 48 | /** 49 | * Returns: True if the given path should be ignored, false otherwise. 50 | */ 51 | private static bool ignorePath(const(char)[] path) pure nothrow 52 | { 53 | import std.algorithm.searching : startsWith; 54 | 55 | foreach (ignored; ignoredPaths) 56 | { 57 | if (path.startsWith(ignored)) 58 | return true; 59 | } 60 | 61 | return false; 62 | } 63 | 64 | private 65 | { 66 | import std.container.rbtree; 67 | 68 | // Current working directories of each tracked process. 69 | string[int] processes; 70 | 71 | RedBlackTree!string inputs, outputs; 72 | } 73 | 74 | void dump(ref Resources implicitInputs, ref Resources implicitOutputs) 75 | { 76 | implicitInputs.put(inputs[]); 77 | implicitOutputs.put(outputs[]); 78 | } 79 | 80 | string filePath(int pid, const(char)[] path) 81 | { 82 | import std.path : buildNormalizedPath; 83 | 84 | if (auto p = pid in processes) 85 | return buildNormalizedPath(*p, path); 86 | 87 | return buildNormalizedPath(path); 88 | } 89 | 90 | void parse(File f) 91 | { 92 | import io.text; 93 | import std.conv : parse, ConvException; 94 | import std.string : stripLeft; 95 | import std.algorithm.searching : startsWith; 96 | import std.regex : matchFirst; 97 | 98 | inputs = redBlackTree!string(); 99 | outputs = redBlackTree!string(); 100 | 101 | foreach (line; f.byLine) 102 | { 103 | int pid; 104 | 105 | try 106 | pid = line.parse!int(); 107 | catch (ConvException e) 108 | continue; 109 | 110 | line = line.stripLeft(" \t"); 111 | 112 | if (line.startsWith("open")) 113 | { 114 | auto captures = line.matchFirst(re_open); 115 | if (captures.empty) 116 | continue; 117 | 118 | open(pid, captures[1], captures[2]); 119 | } 120 | else if (line.startsWith("creat")) 121 | { 122 | auto captures = line.matchFirst(re_open); 123 | if (captures.empty) 124 | continue; 125 | 126 | creat(pid, captures[1]); 127 | } 128 | else if (line.startsWith("rename")) 129 | { 130 | auto captures = line.matchFirst(re_rename); 131 | if (captures.empty) 132 | continue; 133 | 134 | rename(pid, captures[1], captures[2]); 135 | } 136 | else if (line.startsWith("mkdir")) 137 | { 138 | auto captures = line.matchFirst(re_mkdir); 139 | if (captures.empty) 140 | continue; 141 | 142 | mkdir(pid, captures[1]); 143 | } 144 | else if (line.startsWith("chdir")) 145 | { 146 | auto captures = line.matchFirst(re_chdir); 147 | if (captures.empty) 148 | continue; 149 | 150 | chdir(pid, captures[1]); 151 | } 152 | } 153 | } 154 | 155 | void open(int pid, const(char)[] path, const(char)[] flags) 156 | { 157 | import std.algorithm.iteration : splitter; 158 | 159 | if (ignorePath(path)) 160 | return; 161 | 162 | foreach (flag; splitter(flags, '|')) 163 | { 164 | if (flag == "O_WRONLY" || flag == "O_RDWR") 165 | { 166 | // Opened in write mode. It's an output even if it was read 167 | // before. 168 | auto f = filePath(pid, path); 169 | inputs.removeKey(f); 170 | outputs.insert(f); 171 | break; 172 | } 173 | else if (flag == "O_RDONLY") 174 | { 175 | // Opened in read-only mode. It's an input unless it's already 176 | // an output. Consider the scenario of writing a new file and 177 | // then reading it back in. In such cases, the file should only 178 | // be considered an output. 179 | auto f = filePath(pid, path); 180 | if (f !in outputs) 181 | inputs.insert(f); 182 | break; 183 | } 184 | } 185 | } 186 | 187 | void creat(int pid, const(char)[] path) 188 | { 189 | if (ignorePath(path)) 190 | return; 191 | 192 | outputs.insert(filePath(pid, path)); 193 | } 194 | 195 | void rename(int pid, const(char)[] from, const(char)[] to) 196 | { 197 | if (ignorePath(to)) 198 | return; 199 | 200 | auto output = filePath(pid, to); 201 | outputs.removeKey(filePath(pid, from)); 202 | inputs.removeKey(output); 203 | outputs.insert(output); 204 | } 205 | 206 | void mkdir(int pid, const(char)[] dir) 207 | { 208 | outputs.insert(filePath(pid, dir)); 209 | } 210 | 211 | void chdir(int pid, const(char)[] path) 212 | { 213 | processes[pid] = path.idup; 214 | } 215 | } 216 | 217 | void execute( 218 | ref BuildContext ctx, 219 | const(string)[] args, 220 | string workDir, 221 | ref Resources inputs, 222 | ref Resources outputs 223 | ) 224 | { 225 | import button.handlers.base : base = execute; 226 | 227 | import std.file : remove; 228 | 229 | auto traceLog = tempFile(AutoDelete.no).path; 230 | scope (exit) remove(traceLog); 231 | 232 | auto traceArgs = [ 233 | "strace", 234 | 235 | // Follow child processes 236 | "-f", 237 | 238 | // Output to a file to avoid mixing the child's output 239 | "-o", traceLog, 240 | 241 | // Only trace the sys calls we are interested in 242 | "-e", "trace=open,creat,rename,mkdir,chdir", 243 | ] ~ args; 244 | 245 | base(ctx, traceArgs, workDir, inputs, outputs); 246 | 247 | // Parse the trace log to determine dependencies 248 | auto strace = Trace(); 249 | strace.parse(File(traceLog)); 250 | strace.dump(inputs, outputs); 251 | } 252 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/button/resource.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.resource; 7 | import std.digest : DigestType, isDigest; 8 | 9 | import std.array : Appender; 10 | 11 | /** 12 | * A resource identifier. 13 | */ 14 | alias ResourceId = string; 15 | 16 | /** 17 | * A resource key must be unique. 18 | */ 19 | struct ResourceKey 20 | { 21 | /** 22 | * File path to the resource. To ensure uniqueness, this should never be 23 | * changed after construction. 24 | */ 25 | string path; 26 | 27 | /** 28 | * Compares this key with another. 29 | */ 30 | int opCmp()(const auto ref typeof(this) that) const pure nothrow 31 | { 32 | import std.algorithm.comparison : cmp; 33 | return cmp(this.path, that.path); 34 | } 35 | } 36 | 37 | unittest 38 | { 39 | static assert(ResourceKey("abc") == ResourceKey("abc")); 40 | static assert(ResourceKey("abc") < ResourceKey("abcd")); 41 | } 42 | 43 | /** 44 | * Compute the checksum of a file. 45 | */ 46 | private DigestType!Hash digestFile(Hash)(string path) 47 | if (isDigest!Hash) 48 | { 49 | import std.digest : digest; 50 | import io.file : SysException, File, FileFlags; 51 | import io.range : byChunk; 52 | 53 | ubyte[4096] buf; 54 | 55 | try 56 | { 57 | return digest!Hash(File(path, FileFlags.readExisting).byChunk(buf)); 58 | } 59 | catch (SysException e) 60 | { 61 | // This may fail if the given path is a directory. The path could have 62 | // also been deleted. 63 | return typeof(return).init; 64 | } 65 | } 66 | 67 | /** 68 | * Computes a stable checksum for the given directory. 69 | * 70 | * Note that we cannot use std.file.dirEntries here. dirEntries() yields the 71 | * full path to the directory entries. We only want the file name, not the path 72 | * to it. Thus, we're forced to list the directory contents the old fashioned 73 | * way. 74 | */ 75 | version (Posix) 76 | private DigestType!Hash digestDir(Hash)(const(char)* path) 77 | if (isDigest!Hash) 78 | { 79 | import core.stdc.string : strlen; 80 | import std.array : Appender; 81 | import std.algorithm.sorting : sort; 82 | import core.sys.posix.dirent : DIR, dirent, opendir, closedir, readdir; 83 | 84 | Appender!(string[]) entries; 85 | 86 | if (DIR* dir = opendir(path)) 87 | { 88 | scope (exit) closedir(dir); 89 | 90 | while (true) 91 | { 92 | dirent* entry = readdir(dir); 93 | if (!entry) break; 94 | 95 | entries.put(entry.d_name[0 .. strlen(entry.d_name.ptr)].idup); 96 | } 97 | } 98 | else 99 | { 100 | // In this case, this is either not a directory or it doesn't exist. 101 | return typeof(return).init; 102 | } 103 | 104 | // The order in which files are listed is not guaranteed to be sorted. 105 | // Whether or not it is sorted depends on the file system implementation. 106 | // Thus, we sort them to eliminate that potential source of non-determinism. 107 | sort(entries.data); 108 | 109 | Hash digest; 110 | digest.start(); 111 | 112 | foreach (name; entries.data) 113 | { 114 | digest.put(cast(const(ubyte)[])name); 115 | digest.put(cast(ubyte)0); // Null terminator 116 | } 117 | 118 | return digest.finish(); 119 | } 120 | 121 | /** 122 | * Computes a stable checksum for the given directory. 123 | */ 124 | private DigestType!Hash digestDir(Hash)(string path) 125 | if (isDigest!Hash) 126 | { 127 | import std.internal.cstring : tempCString; 128 | 129 | return digestDir!Hash(path.tempCString()); 130 | } 131 | 132 | /** 133 | * A representation of a file on the disk. 134 | */ 135 | struct Resource 136 | { 137 | import std.datetime : SysTime; 138 | import std.digest.sha : SHA256; 139 | 140 | /** 141 | * Digest to use to determine changes. 142 | */ 143 | alias Hash = SHA256; 144 | 145 | enum Status 146 | { 147 | // The state of the resource is not known. 148 | unknown, 149 | 150 | // The path does not exist on disk. 151 | missing, 152 | 153 | // The path refers to a file. 154 | file, 155 | 156 | // The path refers to a directory. 157 | directory, 158 | } 159 | 160 | /** 161 | * File path to the resource. To ensure uniqueness, this should never be 162 | * changed after construction. 163 | */ 164 | ResourceId path; 165 | 166 | /** 167 | * Status of the file. 168 | */ 169 | Status status = Status.unknown; 170 | 171 | /** 172 | * Checksum of the file. 173 | */ 174 | DigestType!Hash checksum; 175 | 176 | this(ResourceId path, Status status = Status.unknown, 177 | const(ubyte[]) checksum = []) pure 178 | { 179 | import std.algorithm.comparison : min; 180 | 181 | this.path = path; 182 | this.status = status; 183 | 184 | // The only times the length will be different are: 185 | // - The database is corrupt 186 | // - The digest length changed 187 | // In either case, it doesn't matter. If the checksum changes it will 188 | // simply be recomputed and order will once again be restored in the 189 | // realm. 190 | immutable bytes = min(this.checksum.length, checksum.length); 191 | this.checksum[0 .. bytes] = checksum[0 .. bytes]; 192 | } 193 | 194 | /** 195 | * Returns a string representation of this resource. This is just the path 196 | * to the resource. 197 | */ 198 | string toString() const pure nothrow 199 | { 200 | return path; 201 | } 202 | 203 | /** 204 | * Returns a short string representation of the path. 205 | */ 206 | @property string toShortString() const pure nothrow 207 | { 208 | import std.path : baseName; 209 | return path.baseName; 210 | } 211 | 212 | /** 213 | * Returns the unique identifier for this vertex. 214 | */ 215 | @property inout(ResourceId) identifier() inout pure nothrow 216 | { 217 | return path; 218 | } 219 | 220 | /** 221 | * Compares the file path of this resource with another. 222 | */ 223 | int opCmp()(const auto ref Resource rhs) const pure 224 | { 225 | import std.path : filenameCmp; 226 | return filenameCmp(this.path, rhs.path); 227 | } 228 | 229 | /// Ditto 230 | bool opEquals()(const auto ref Resource rhs) const pure 231 | { 232 | return opCmp(rhs) == 0; 233 | } 234 | 235 | unittest 236 | { 237 | assert(Resource("a") < Resource("b")); 238 | assert(Resource("b") > Resource("a")); 239 | 240 | assert(Resource("test", Resource.Status.unknown) == 241 | Resource("test", Resource.Status.unknown)); 242 | assert(Resource("test", Resource.Status.file) == 243 | Resource("test", Resource.Status.directory)); 244 | } 245 | 246 | /** 247 | * Updates the last modified time and checksum of this resource. Returns 248 | * true if anything changed. 249 | * 250 | * Note that the checksum is not recomputed if the modification time is the 251 | * same. 252 | */ 253 | bool update() 254 | { 255 | version (Posix) 256 | { 257 | import core.sys.posix.sys.stat : lstat, stat_t, S_IFMT, S_IFDIR, 258 | S_IFREG; 259 | import io.file.stream : SysException; 260 | import core.stdc.errno : errno, ENOENT; 261 | import std.datetime : unixTimeToStdTime; 262 | import std.internal.cstring : tempCString; 263 | 264 | stat_t statbuf = void; 265 | 266 | auto tmpPath = path.tempCString(); 267 | 268 | Status newStatus; 269 | DigestType!Hash newChecksum; 270 | 271 | if (lstat(tmpPath, &statbuf) != 0) 272 | { 273 | if (errno == ENOENT) 274 | newStatus = Status.missing; 275 | else 276 | throw new SysException("Failed to stat resource"); 277 | } 278 | else if ((statbuf.st_mode & S_IFMT) == S_IFREG) 279 | { 280 | newChecksum = digestFile!Hash(path); 281 | newStatus = Status.file; 282 | } 283 | else if ((statbuf.st_mode & S_IFMT) == S_IFDIR) 284 | { 285 | newChecksum = digestDir!Hash(tmpPath); 286 | newStatus = Status.directory; 287 | } 288 | else 289 | { 290 | // The resource is neither a file nor a directory. It could be a 291 | // special file such as a FIFO, block device, etc. In those 292 | // cases, we cannot be expected to track changes to those types 293 | // of files. 294 | newStatus = Status.unknown; 295 | } 296 | 297 | if (newStatus != status || checksum != newChecksum) 298 | { 299 | status = newStatus; 300 | checksum = newChecksum; 301 | return true; 302 | } 303 | 304 | return false; 305 | } 306 | else 307 | { 308 | static assert(false, "Not implemented yet."); 309 | } 310 | } 311 | 312 | /** 313 | * Returns true if the status of this resource is known. 314 | */ 315 | @property bool statusKnown() const pure nothrow 316 | { 317 | return status != Status.unknown; 318 | } 319 | 320 | /** 321 | * Deletes the resource from disk. 322 | */ 323 | void remove(bool dryRun) nothrow 324 | { 325 | import std.file : unlink = remove, isFile; 326 | import io; 327 | 328 | // Only delete this file if we know about it. This helps prevent the 329 | // build system from haphazardly deleting files that were added to the 330 | // build description but never output by a task. 331 | if (!statusKnown) 332 | return; 333 | 334 | // TODO: Use rmdir instead if this is a directory. 335 | 336 | if (!dryRun) 337 | { 338 | try 339 | { 340 | unlink(path); 341 | } 342 | catch (Exception e) 343 | { 344 | } 345 | } 346 | 347 | status = Status.missing; 348 | } 349 | } 350 | 351 | /** 352 | * Normalizes a resource path while trying to make it relative to the buildRoot. 353 | * If it cannot be done, the path is made absolute. 354 | * 355 | * Params: 356 | * buildRoot = The root directory of the build. Probably always the current 357 | * working directory. 358 | * taskDir = The working directory of the task this is for. The path is 359 | * normalized relative to this directory. 360 | * path = The path to be normalized. 361 | */ 362 | string normPath(const(char)[] buildRoot, const(char)[] taskDir, 363 | const(char)[] path) pure 364 | { 365 | import std.path : isAbsolute, buildNormalizedPath, pathSplitter, 366 | filenameCmp, dirSeparator; 367 | import std.algorithm.searching : skipOver; 368 | import std.algorithm.iteration : joiner; 369 | import std.array : array; 370 | import std.utf : byChar; 371 | 372 | auto normalized = buildNormalizedPath(taskDir, path); 373 | 374 | // If the normalized path is absolute, get a relative path if the absolute 375 | // path is inside the working directory. This is done instead of always 376 | // getting a relative path because we don't want to get relative paths to 377 | // directories like "/usr/include". If the build directory moves, absolute 378 | // paths outside will become invalid. 379 | if (isAbsolute(normalized) && buildRoot.length) 380 | { 381 | auto normPS = pathSplitter(normalized); 382 | auto buildPS = pathSplitter(buildRoot); 383 | 384 | alias pred = (a, b) => filenameCmp(a, b) == 0; 385 | 386 | if (skipOver!pred(normPS, &buildPS) && buildPS.empty) 387 | return normPS.joiner(dirSeparator).byChar.array; 388 | } 389 | 390 | return normalized; 391 | } 392 | 393 | pure unittest 394 | { 395 | version (Posix) 396 | { 397 | assert(normPath("", "", "foo") == "foo"); 398 | assert(normPath("", "foo", "bar") == "foo/bar"); 399 | 400 | assert(normPath("", "foo/../foo/.", "bar/../baz") == "foo/baz"); 401 | 402 | assert(normPath("", "foo", "/usr/include/bar") == "/usr/include/bar"); 403 | assert(normPath("/usr", "foo", "/usr/bar") == "bar"); 404 | assert(normPath("/usr/include", "foo", "/usr/bar") == "/usr/bar"); 405 | } 406 | } 407 | 408 | /** 409 | * Output range of implicit resources. 410 | * 411 | * This is used to easily accumulate implicit resources while also normalizing 412 | * their paths at the same time. 413 | */ 414 | struct Resources 415 | { 416 | import std.array : Appender; 417 | import std.range : isInputRange, ElementType; 418 | 419 | Appender!(Resource[]) resources; 420 | 421 | alias resources this; 422 | 423 | string buildDir; 424 | string taskDir; 425 | 426 | this(string buildDir, string taskDir) 427 | { 428 | this.buildDir = buildDir; 429 | this.taskDir = taskDir; 430 | } 431 | 432 | void put(R)(R items) 433 | if (isInputRange!R && is(ElementType!R : const(char)[])) 434 | { 435 | import std.range : empty, popFront, front; 436 | 437 | for (; !items.empty; items.popFront()) 438 | put(items.front); 439 | } 440 | 441 | void put(const(char)[] item) 442 | { 443 | resources.put(Resource(normPath(buildDir, taskDir, item))); 444 | } 445 | 446 | void put(R)(R items) 447 | if (isInputRange!R && is(ElementType!R : Resource)) 448 | { 449 | resources.put(items); 450 | } 451 | 452 | void put(Resource item) 453 | { 454 | item.path = normPath(buildDir, taskDir, item.path); 455 | resources.put(item); 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /source/button/rule.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Parses rules. 8 | */ 9 | module button.rule; 10 | 11 | import std.range.primitives : isInputRange, ElementType; 12 | 13 | import button.command; 14 | import button.resource; 15 | import button.task; 16 | import button.edge; 17 | 18 | struct Rule 19 | { 20 | /** 21 | * The sets of inputs and outputs that this task is dependent on. 22 | */ 23 | Resource[] inputs, outputs; 24 | 25 | /** 26 | * The command to execute. 27 | */ 28 | Task task; 29 | } 30 | 31 | struct Rules 32 | { 33 | import std.json : JSONValue; 34 | 35 | private 36 | { 37 | JSONValue[] rules; 38 | 39 | // Current rule taken from the stream. 40 | Rule rule; 41 | 42 | bool _empty; 43 | } 44 | 45 | this(JSONValue rules) 46 | { 47 | this.rules = rules.array(); 48 | 49 | // Prime the cannon 50 | popFront(); 51 | } 52 | 53 | void popFront() 54 | { 55 | import std.range : empty, popFront, front; 56 | import std.algorithm : map; 57 | import std.array : array; 58 | import std.json : JSONException; 59 | import std.path : buildNormalizedPath; 60 | import std.exception : assumeUnique; 61 | 62 | if (rules.empty) 63 | { 64 | _empty = true; 65 | return; 66 | } 67 | 68 | auto jsonRule = rules.front; 69 | 70 | auto inputs = jsonRule["inputs"].array() 71 | .map!(x => Resource(buildNormalizedPath(x.str()))) 72 | .array(); 73 | 74 | auto outputs = jsonRule["outputs"].array() 75 | .map!(x => Resource(buildNormalizedPath(x.str()))) 76 | .array(); 77 | 78 | auto commands = jsonRule["task"].array() 79 | .map!(x => Command(x.array().map!(y => y.str()).array().idup)) 80 | .array 81 | .assumeUnique; 82 | 83 | string cwd = ""; 84 | 85 | // Optional 86 | try 87 | cwd = jsonRule["cwd"].str(); 88 | catch(JSONException e) {} 89 | 90 | string display; 91 | try 92 | display = jsonRule["display"].str(); 93 | catch(JSONException e) {} 94 | 95 | rule = Rule(inputs, outputs, Task(commands, cwd, display)); 96 | 97 | rules.popFront(); 98 | } 99 | 100 | inout(Rule) front() inout 101 | { 102 | return rule; 103 | } 104 | 105 | bool empty() const pure nothrow 106 | { 107 | return _empty; 108 | } 109 | } 110 | 111 | /** 112 | * Convenience function for constructing a Rules range. 113 | */ 114 | @property Rules parseRules(R)(R json) 115 | if (isInputRange!R) 116 | { 117 | import std.json : parseJSON; 118 | return Rules(parseJSON(json)); 119 | } 120 | 121 | unittest 122 | { 123 | import std.algorithm : equal; 124 | 125 | immutable json = q{ 126 | [ 127 | { 128 | "inputs": ["foo.c", "baz.h"], 129 | "task": [["gcc", "-c", "foo.c", "-o", "foo.o"]], 130 | "display": "cc foo.c", 131 | "outputs": ["foo.o"] 132 | }, 133 | { 134 | "inputs": ["bar.c", "baz.h"], 135 | "task": [["gcc", "-c", "bar.c", "-o", "bar.o"]], 136 | "outputs": ["bar.o"] 137 | }, 138 | { 139 | "inputs": ["foo.o", "bar.o"], 140 | "task": [["gcc", "foo.o", "bar.o", "-o", "foobar"]], 141 | "outputs": ["foobar"] 142 | } 143 | ] 144 | }; 145 | 146 | immutable Rule[] rules = [ 147 | { 148 | inputs: [Resource("foo.c"), Resource("baz.h")], 149 | task: Task([Command(["gcc", "-c", "foo.c", "-o", "foo.o"])]), 150 | outputs: [Resource("foo.o")] 151 | }, 152 | { 153 | inputs: [Resource("bar.c"), Resource("baz.h")], 154 | task: Task([Command(["gcc", "-c", "bar.c", "-o", "bar.o"])]), 155 | outputs: [Resource("bar.o")] 156 | }, 157 | { 158 | inputs: [Resource("foo.o"), Resource("bar.o")], 159 | task: Task([Command(["gcc", "foo.o", "bar.o", "-o", "foobar"])]), 160 | outputs: [Resource("foobar")] 161 | } 162 | ]; 163 | 164 | assert(parseRules(json).equal(rules)); 165 | } 166 | 167 | unittest 168 | { 169 | import std.algorithm : equal; 170 | 171 | immutable json = q{ 172 | [ 173 | { 174 | "inputs": ["./test/../foo.c", "./baz.h"], 175 | "task": [["ls", "foo.c", "baz.h"]], 176 | "outputs": ["this/../path/../is/../normalized"] 177 | } 178 | ] 179 | }; 180 | 181 | immutable Rule[] rules = [ 182 | { 183 | inputs: [Resource("foo.c"), Resource("baz.h")], 184 | task: Task([Command(["ls", "foo.c", "baz.h"])]), 185 | outputs: [Resource("normalized")] 186 | }, 187 | ]; 188 | 189 | assert(parseRules(json).equal(rules)); 190 | } 191 | -------------------------------------------------------------------------------- /source/button/task.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.task; 7 | 8 | import button.command; 9 | import button.resource; 10 | import button.context; 11 | import button.exceptions; 12 | 13 | /** 14 | * A task key must be unique. 15 | */ 16 | struct TaskKey 17 | { 18 | /** 19 | * The commands to execute in sequential order. The first argument is the 20 | * name of the executable. 21 | */ 22 | immutable(Command)[] commands; 23 | 24 | /** 25 | * The working directory for the commands, relative to the current working 26 | * directory of the build system. If empty, the current working directory of 27 | * the build system is used. 28 | */ 29 | string workingDirectory = ""; 30 | 31 | this(immutable(Command)[] commands, string workingDirectory = "") 32 | { 33 | assert(commands.length, "A task must have >0 commands"); 34 | 35 | this.commands = commands; 36 | this.workingDirectory = workingDirectory; 37 | } 38 | 39 | /** 40 | * Compares this key with another. 41 | */ 42 | int opCmp()(const auto ref typeof(this) that) const pure nothrow 43 | { 44 | import std.algorithm.comparison : cmp; 45 | import std.path : filenameCmp; 46 | 47 | if (immutable result = cmp(this.commands, that.commands)) 48 | return result; 49 | 50 | return filenameCmp(this.workingDirectory, that.workingDirectory); 51 | } 52 | 53 | /// Ditto 54 | bool opEquals()(const auto ref typeof(this) that) const pure nothrow 55 | { 56 | return this.opCmp(that) == 0; 57 | } 58 | } 59 | 60 | unittest 61 | { 62 | // Comparison 63 | static assert(TaskKey([Command(["a", "b"])]) < TaskKey([Command(["a", "c"])])); 64 | static assert(TaskKey([Command(["a", "c"])]) > TaskKey([Command(["a", "b"])])); 65 | static assert(TaskKey([Command(["a", "b"])], "a") == TaskKey([Command(["a", "b"])], "a")); 66 | static assert(TaskKey([Command(["a", "b"])], "a") != TaskKey([Command(["a", "b"])], "b")); 67 | static assert(TaskKey([Command(["a", "b"])], "a") < TaskKey([Command(["a", "b"])], "b")); 68 | } 69 | 70 | unittest 71 | { 72 | import std.conv : to; 73 | 74 | // Converting commands to a string. This is used to store/retrieve tasks in 75 | // the database. 76 | 77 | immutable t = TaskKey([ 78 | Command(["foo", "bar"]), 79 | Command(["baz"]), 80 | ]); 81 | 82 | assert(t.commands.to!string == `[["foo", "bar"], ["baz"]]`); 83 | } 84 | 85 | /** 86 | * A representation of a task. 87 | */ 88 | struct Task 89 | { 90 | import std.datetime : SysTime; 91 | 92 | TaskKey key; 93 | 94 | alias key this; 95 | 96 | /** 97 | * Time this task was last executed. If this is SysTime.min, then it is 98 | * taken to mean that the task has never been executed before. This is 99 | * useful for knowing if a task with no dependencies needs to be executed. 100 | */ 101 | SysTime lastExecuted = SysTime.min; 102 | 103 | /** 104 | * Text to display when running the task. If this is null, the commands 105 | * themselves will be displayed. This is useful for reducing the amount of 106 | * noise that is displayed. 107 | */ 108 | string display; 109 | 110 | /** 111 | * The result of executing a task. 112 | */ 113 | struct Result 114 | { 115 | /** 116 | * List of raw byte arrays of implicit inputs/outputs. There is one byte 117 | * array per command. 118 | */ 119 | Resource[] inputs, outputs; 120 | } 121 | 122 | this(TaskKey key) 123 | { 124 | this.key = key; 125 | } 126 | 127 | this(immutable(Command)[] commands, string workDir = "", 128 | string display = null, SysTime lastExecuted = SysTime.min) 129 | { 130 | assert(commands.length, "A task must have >0 commands"); 131 | 132 | this.commands = commands; 133 | this.display = display; 134 | this.workingDirectory = workDir; 135 | this.lastExecuted = lastExecuted; 136 | } 137 | 138 | /** 139 | * Returns a string representation of the task. 140 | * 141 | * Since individual commands are in argv format, we format it into a string 142 | * as one would enter into a shell. 143 | */ 144 | string toPrettyString(bool verbose = false) const pure 145 | { 146 | import std.array : join; 147 | import std.algorithm.iteration : map; 148 | 149 | if (display && !verbose) 150 | return display; 151 | 152 | // Just use the first command 153 | return commands[0].toPrettyString; 154 | } 155 | 156 | /** 157 | * Returns a short string representation of the task. 158 | */ 159 | @property string toPrettyShortString() const pure nothrow 160 | { 161 | if (display) 162 | return display; 163 | 164 | // Just use the first command 165 | return commands[0].toPrettyShortString; 166 | } 167 | 168 | /** 169 | * Compares this task with another. 170 | */ 171 | int opCmp()(const auto ref typeof(this) that) const pure nothrow 172 | { 173 | return this.key.opCmp(that.key); 174 | } 175 | 176 | /// Ditto 177 | bool opEquals()(const auto ref typeof(this) that) const pure nothrow 178 | { 179 | return opCmp(that) == 0; 180 | } 181 | 182 | version (none) unittest 183 | { 184 | assert(Task([["a", "b"]]) < Task([["a", "c"]])); 185 | assert(Task([["a", "b"]]) > Task([["a", "a"]])); 186 | 187 | assert(Task([["a", "b"]]) < Task([["a", "c"]])); 188 | assert(Task([["a", "b"]]) > Task([["a", "a"]])); 189 | 190 | assert(Task([["a", "b"]]) == Task([["a", "b"]])); 191 | assert(Task([["a", "b"]], "a") < Task([["a", "b"]], "b")); 192 | assert(Task([["a", "b"]], "b") > Task([["a", "b"]], "a")); 193 | assert(Task([["a", "b"]], "a") == Task([["a", "b"]], "a")); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/inotify.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | */ 6 | module button.watcher.inotify; 7 | 8 | version (linux): 9 | 10 | import button.state; 11 | import button.resource; 12 | 13 | import core.sys.posix.unistd; 14 | import core.sys.posix.poll; 15 | import core.sys.linux.sys.inotify; 16 | 17 | extern (C) { 18 | private size_t strnlen(const(char)* s, size_t maxlen); 19 | } 20 | 21 | /** 22 | * Wrapper for an inotify_event. 23 | */ 24 | private struct Event 25 | { 26 | // Maximum size that an inotify_event can b. This is used to determine a 27 | // good buffer size. 28 | static immutable max = inotify_event.sizeof * 256; 29 | 30 | int wd; 31 | uint mask; 32 | uint cookie; 33 | const(char)[] name; 34 | 35 | this(inotify_event* e) 36 | { 37 | wd = e.wd; 38 | mask = e.mask; 39 | cookie = e.cookie; 40 | name = e.name.ptr[0 .. strnlen(e.name.ptr, e.len)]; 41 | } 42 | } 43 | 44 | /** 45 | * An infinite input range of chunks of changes. Each item in the range is an 46 | * array of changed resources. That is, for each item in the range, a new build 47 | * should be started. If many files are changed over a short period of time 48 | * (depending on the delay), they will be included in one chunk. 49 | */ 50 | struct ChangeChunks 51 | { 52 | private 53 | { 54 | import std.array : Appender; 55 | 56 | // inotify file descriptor 57 | int fd = -1; 58 | 59 | enum maxEvents = 32; 60 | 61 | BuildState state; 62 | 63 | Appender!(Index!Resource[]) current; 64 | 65 | // Mapping of watches to directories. This is needed to find the path to 66 | // the directory that is being watched. 67 | string[int] watches; 68 | 69 | // Number of milliseconds to wait. Wait indefinitely by default. 70 | int delay = -1; 71 | } 72 | 73 | // This is an infinite range. 74 | enum empty = false; 75 | 76 | this(BuildState state, string watchDir, size_t delay) 77 | { 78 | import std.path : filenameCmp, dirName, buildNormalizedPath; 79 | import std.container.rbtree; 80 | import std.file : exists; 81 | import core.sys.linux.sys.inotify; 82 | import io.file.stream : sysEnforce; 83 | import std.conv : to; 84 | 85 | this.state = state; 86 | 87 | if (delay == 0) 88 | this.delay = -1; 89 | else 90 | this.delay = delay.to!int; 91 | 92 | fd = inotify_init1(IN_NONBLOCK); 93 | sysEnforce(fd != -1, "Failed to initialize inotify"); 94 | 95 | alias less = (a,b) => filenameCmp(a, b) < 0; 96 | 97 | auto rbt = redBlackTree!(less, string)(); 98 | 99 | // Find all directories. 100 | foreach (key; state.enumerate!ResourceKey) 101 | rbt.insert(dirName(key.path)); 102 | 103 | // Watch each (unique) directory. Note that we only watch directories 104 | // instead of individual files so that we are less likely to run out of 105 | // file descriptors. Later, we filter out events for files we are not 106 | // interested in. 107 | foreach (dir; rbt[]) 108 | { 109 | auto realDir = buildNormalizedPath(watchDir, dir); 110 | 111 | if (exists(realDir)) 112 | { 113 | auto watch = addWatch(realDir, 114 | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE); 115 | watches[watch] = dir; 116 | } 117 | } 118 | 119 | popFront(); 120 | } 121 | 122 | ~this() 123 | { 124 | if (fd != -1) 125 | close(fd); 126 | } 127 | 128 | /** 129 | * Adds a path to be watched by inotify. 130 | */ 131 | private int addWatch(const(char)[] path, uint mask = IN_ALL_EVENTS) 132 | { 133 | import std.internal.cstring : tempCString; 134 | import io.file.stream : sysEnforce; 135 | import std.format : format; 136 | 137 | immutable wd = inotify_add_watch(fd, path.tempCString(), mask); 138 | 139 | sysEnforce(wd != -1, "Failed to watch path '%s'".format(path)); 140 | 141 | return wd; 142 | } 143 | 144 | /** 145 | * Removes a watch from inotify. 146 | */ 147 | private void removeWatch(int wd) 148 | { 149 | inotify_rm_watch(fd, wd); 150 | } 151 | 152 | /** 153 | * Returns an array of resource indices that have been (potentially) 154 | * modified. They still need to be checked to determine if their contents 155 | * changed. 156 | */ 157 | const(Index!Resource)[] front() 158 | { 159 | return current.data; 160 | } 161 | 162 | /** 163 | * Called when events are ready to be read. 164 | */ 165 | private void handleEvents(ubyte[] buf) 166 | { 167 | import std.path : buildNormalizedPath; 168 | import io.file.stream : SysException; 169 | import core.stdc.errno : errno, EAGAIN; 170 | 171 | // Window into the valid region of the buffer. 172 | ubyte[] window; 173 | 174 | while (true) 175 | { 176 | immutable len = read(fd, buf.ptr, buf.length); 177 | if (len == -1) 178 | { 179 | // Nothing more to read, break out of the loop. 180 | if (errno == EAGAIN) 181 | break; 182 | 183 | throw new SysException("Failed to read inotify events"); 184 | } 185 | 186 | window = buf[0 .. len]; 187 | 188 | // Loop over the events 189 | while (window.length) 190 | { 191 | auto e = cast(inotify_event*)window.ptr; 192 | auto event = Event(e); 193 | 194 | auto path = buildNormalizedPath(watches[event.wd], event.name); 195 | 196 | // Since we monitor directories and not specific files, we must 197 | // check if we received a change that we are actually interested in. 198 | auto id = state.find(path); 199 | if (id != Index!Resource.Invalid) 200 | current.put(id); 201 | 202 | window = window[inotify_event.sizeof + e.len .. $]; 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * Accumulates changes. 209 | */ 210 | void popFront() 211 | { 212 | import io.file.stream : SysException; 213 | import core.stdc.errno : errno, EINTR; 214 | 215 | pollfd[1] pollFds = [pollfd(fd, POLLIN)]; 216 | 217 | // Buffer to hold the events. Multiple events can be read at a time. 218 | ubyte[maxEvents * Event.max] buf; 219 | 220 | current.clear(); 221 | 222 | while (true) 223 | { 224 | // Wait for more events. If we haven't received any yet, wait 225 | // indefinitely. Otherwise, give up after a certain delay and return 226 | // what we've received. 227 | immutable n = poll(pollFds.ptr, pollFds.length, 228 | current.data.length ? delay : -1); 229 | if (n == -1) 230 | { 231 | if (errno == EINTR) 232 | continue; 233 | 234 | throw new SysException("Failed to poll for inotify events"); 235 | } 236 | else if (n == 0) 237 | { 238 | // Poll timed out and we've got events, so lets use them. 239 | if (current.data.length > 0) 240 | break; 241 | } 242 | else if (n > 0) 243 | { 244 | if (pollFds[0].revents & POLLIN) 245 | handleEvents(buf); 246 | 247 | // Can't ever time out. Yield any events we have. 248 | if (delay == -1 && current.data.length > 0) 249 | break; 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/util/change.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright: Copyright Jason White, 2016 3 | * License: MIT 4 | * Authors: Jason White 5 | * 6 | * Description: 7 | * Finds differences between two ranges. 8 | * 9 | * All changes are discovered in O(max(n, m)) where n and m are the length of 10 | * the two ranges. 11 | */ 12 | module util.change; 13 | 14 | import std.range : isInputRange, ElementType; 15 | 16 | /** 17 | * Type of a change. 18 | */ 19 | enum ChangeType 20 | { 21 | none, 22 | added, 23 | removed 24 | } 25 | 26 | /** 27 | * Describes a change. 28 | */ 29 | struct Change(T) 30 | { 31 | T value; 32 | ChangeType type; 33 | } 34 | 35 | /** 36 | * Range for iterating over changes between two sorted ranges. 37 | */ 38 | struct Changes(R1, R2, alias pred = "a < b") 39 | if (isInputRange!R1 && isInputRange!R2 && 40 | is(ElementType!R1 == ElementType!R2)) 41 | { 42 | import std.range : ElementType; 43 | import std.traits : Unqual; 44 | import std.typecons : Rebindable; 45 | 46 | private alias E = ElementType!R1; 47 | 48 | static if ((is(E == class) || is(E == interface)) && 49 | (is(E == const) || is(E == immutable))) 50 | { 51 | private alias MutableE = Rebindable!E; 52 | } 53 | else static if (is(E : Unqual!E)) 54 | { 55 | private alias MutableE = Unqual!E; 56 | } 57 | else 58 | { 59 | private alias MutableE = E; 60 | } 61 | 62 | private 63 | { 64 | // Current change. 65 | Change!MutableE _current; 66 | 67 | // Next and previous states. 68 | R1 prev; 69 | R2 next; 70 | 71 | bool _empty; 72 | } 73 | 74 | this(R1 prev, R2 next) 75 | { 76 | this.prev = prev; 77 | this.next = next; 78 | 79 | popFront(); 80 | } 81 | 82 | void popFront() 83 | { 84 | import std.range : empty, front, popFront; 85 | import std.functional : binaryFun; 86 | 87 | if (prev.empty && next.empty) 88 | { 89 | _empty = true; 90 | } 91 | else if (prev.empty) 92 | { 93 | _current = Change!E(next.front, ChangeType.added); 94 | next.popFront(); 95 | } 96 | else if (next.empty) 97 | { 98 | _current = Change!E(prev.front, ChangeType.removed); 99 | prev.popFront(); 100 | } 101 | else 102 | { 103 | auto a = prev.front; 104 | auto b = next.front; 105 | 106 | if (binaryFun!pred(a, b)) 107 | { 108 | // Removed 109 | _current = Change!E(a, ChangeType.removed); 110 | prev.popFront(); 111 | } 112 | else if (binaryFun!pred(b, a)) 113 | { 114 | // Added 115 | _current = Change!E(b, ChangeType.added); 116 | next.popFront(); 117 | } 118 | else 119 | { 120 | // No change 121 | _current = Change!E(a, ChangeType.none); 122 | prev.popFront(); 123 | next.popFront(); 124 | } 125 | } 126 | } 127 | 128 | @property auto ref front() pure nothrow 129 | { 130 | return _current; 131 | } 132 | 133 | bool empty() const pure nothrow 134 | { 135 | return _empty; 136 | } 137 | } 138 | 139 | /** 140 | * Convenience function for constructing a range that finds changes between two 141 | * ranges. 142 | */ 143 | auto changes(alias pred = "a < b", R1, R2)(R1 previous, R2 next) 144 | { 145 | return Changes!(R1, R2, pred)(previous, next); 146 | } 147 | 148 | unittest 149 | { 150 | import std.algorithm : equal; 151 | 152 | immutable prev = "abcd"; 153 | immutable next = "acdef"; 154 | 155 | immutable Change!dchar[] result = [ 156 | {'a', ChangeType.none}, 157 | {'b', ChangeType.removed}, 158 | {'c', ChangeType.none}, 159 | {'d', ChangeType.none}, 160 | {'e', ChangeType.added}, 161 | {'f', ChangeType.added}, 162 | ]; 163 | 164 | assert(result.equal(changes(prev, next))); 165 | } 166 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------