├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── contributing.md ├── lake ├── shard.yml ├── spec ├── _purge_spec.cr ├── build_spec.cr ├── run_spec.cr └── spec_helper.cr └── src ├── lake.cr └── lake ├── builder.cr ├── exception.cr ├── finder.cr ├── runner.cr └── version.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /.crystal/ 4 | /.shards/ 5 | Lakefile 6 | /.lake 7 | .ruby-version 8 | create.txt 9 | 10 | # Libraries don't need dependency lock 11 | # Dependencies will be locked in application that uses them 12 | /shard.lock 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: required 3 | before_install: | 4 | curl http://dist.crystal-lang.org/apt/setup.sh | sudo bash 5 | sudo apt-get -q update 6 | install: | 7 | sudo apt-get install crystal 8 | crystal deps 9 | script: 10 | - crystal spec 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Adler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lake [![Build Status](https://travis-ci.org/adlerhsieh/lake.svg?branch=master)](https://travis-ci.org/adlerhsieh/lake) 2 | 3 | #### Rake is productive, but we want it faster. 4 | 5 | Lake is a [rake](http://rake.rubyforge.org/)-inspired tool in Crystal-lang for managing you tasks. Tasks are automatically built & run through the command line interface. It take advantages of the performance of `Crystal` and the utility of `rake`, helping you run recursive tasks in amazing speed. 6 | 7 | ## Features 8 | 9 | - Automatically building & running tasks. 10 | - Managing taks in `Lakefile` or `.lake` directory. 11 | - Use it with `cron` and other automation tools for more efficient workflows. 12 | 13 | ## Requirement 14 | 15 | [Crystal](https://github.com/manastech/crystal) >= 0.9.0. If you're on Mac OS X installing with Homebrew, Lake will install Crystal for you. 16 | 17 | ## Installtion 18 | 19 | | System | Available Methods | 20 | | -------- | ------------------- | 21 | | All | [Manual Installation](https://github.com/adlerhsieh/lake#manual-installation) | 22 | | OSX | [Homebrew](https://github.com/adlerhsieh/lake#mac-os-x) | 23 | | Ubuntu / Debian | Work in progress | 24 | | Windows | Not Supported | 25 | 26 | #### Mac OS X 27 | 28 | ``` 29 | brew tap adlerhsieh/lake 30 | brew update 31 | brew install lake 32 | ``` 33 | 34 | [Installtion details](https://github.com/adlerhsieh/homebrew-lake) 35 | 36 | #### Manual Installation 37 | 38 | 1. [Install Crystal](http://crystal-lang.org/docs/installation/from_source_repository.html). 39 | 2. Download the latest lake [executable](https://github.com/adlerhsieh/lake/raw/master/lake). 40 | 3. Move the executable to one of your `PATH` directory, e.g. `/usr/local/bin`. 41 | 4. Run `lake -v` and `crystal -v` to see if the installation is successful. 42 | 43 | ## Usage 44 | 45 | #### Create your first task 46 | 47 | Create a `Lakefile` in any project directory: 48 | 49 | ```crystal 50 | Task.hello # This is task name 51 | puts "hello world" # This is task content 52 | ``` 53 | 54 | This creates a task named `hello`. Run: 55 | 56 | ``` 57 | lake hello 58 | ``` 59 | 60 | It compiles and build a task file for `hello` task. You should see `hello world` on screen and that's it. Write any script you want and run it this way. 61 | 62 | #### Writing mulitple tasks in a single file 63 | 64 | ```crystal 65 | Task.salute 66 | puts "salute!" 67 | 68 | Task.write 69 | File.write("./story.txt", "Mary has a little lamb.") 70 | ``` 71 | 72 | Each `Task` forms a block that runs the code inside. It is not a Crystal block so it allows defining a class and method in the code as in normal Crystal context. 73 | 74 | #### Dependencies 75 | 76 | If you're using dependencies, require them in the task block like: 77 | 78 | ```crystal 79 | Task.query 80 | require "crystal-mysql" 81 | ``` 82 | 83 | Lake shares dependencies with your project, so run `lake` command in the project root directory where `libs` and `.shards` directory exist. 84 | 85 | #### Second time is faster 86 | 87 | The first time you run a task is a bit slower, but the second time is blazingly fast. It is because Crystal is a compiled language, so it is necessary to build a task before running it. Lake automatically checks for change in all tasks and only build tasks that have any change. 88 | 89 | #### Work with multiple files 90 | 91 | If you have many tasks in a project, separate them in different files. In addition to `Lakefile`, you can add any `.cr` file in `.lake` directory. All `.cr` files in the directory will be considered lake tasks. 92 | 93 | ## Options 94 | 95 | | Short Flag | Long Flag | Description 96 | |----------- |-------------|----------- | 97 | |`-b` |`--build` | Builds all tasks | 98 | |`-r` |`--rebuild` | Rebuilds all tasks | 99 | |`-h` |`--help` | Displays help messages | 100 | |`-v` |`--version` | Displays current version | 101 | 102 | ## Progress 103 | 104 | ##### 0.1.0 105 | - [x] Allow processing & executing tasks in `.lake` directory 106 | - [x] Allow processing & executing `Lakefile` 107 | - [x] Allow `Lakefile` and `.lake` directory generation 108 | - [x] Brew installation 109 | - [x] Usage & Instructions 110 | 111 | ##### 0.2.0 112 | - [x] Remove failed build task in `tasks` directory 113 | - [x] Allow executing multiple tasks in one command 114 | - [x] Remove reduntant `-t` when executing command 115 | - [x] Setting up ci service 116 | - [x] Automatically install Crystal before installing Lake 117 | - [x] Allow `shards` support in `.lake` 118 | - [x] Allow dependency requirement 119 | - [x] Manual installation 120 | 121 | ##### 0.3.0 122 | - [ ] Argument support for tasks 123 | - [ ] Allow symbols except "-" in task name, all separated by a single blank space 124 | - [ ] DSL support that allows putting multiple tasks in a single task 125 | - [ ] Allow looking for other `Lakefile`s if not in current directory 126 | - [ ] Allow global Lakefile and `-g` option 127 | 128 | ##### 1.0.0 129 | - [ ] apt-get installation 130 | - [ ] Unit Test 131 | - [ ] Acceptance Test 132 | - [ ] Official website or something better than README as introduction (gh-pages or .org) 133 | 134 | ##### In the future 135 | - [ ] Auto-detect non-character in task name and send warning 136 | - [ ] Allow checking if `pwd` is in a git repo, crystal project, and has a Lakefile. 137 | 138 | ## Contributing 139 | 140 | Read the [Contributing guide](contributing.md) 141 | 142 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Preparation 4 | 5 | 1. [Fork it](https://github.com/adlerhsieh/lake/fork) 6 | 2. Create your feature branch (git checkout -b my-new-feature) 7 | 8 | Before you start to make any change, run `shards` to install dependencies, they are used for a proportion of tests. 9 | 10 | ## Running tests 11 | 12 | After you make some changes, run tests and make sure they all passed. 13 | 14 | Note that acceptance tests actually run the commands, so there will be a lot of output messages, but only those with `Failure`, `Error`, and `Task ignored` are failed tests. 15 | 16 | ## Pull Requests 17 | 18 | As contributing to any project, after changes are made: 19 | 20 | 1. Commit your changes (git commit -am 'Add some feature') 21 | 2. Push to the branch (git push origin my-new-feature) 22 | 3. Create a new Pull Request 23 | 4. Make sure all tests are passed on Travis CI. 24 | -------------------------------------------------------------------------------- /lake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adlerhsieh/lake/f7f0c69351448c8adae5d1322a6d427b983e1fc5/lake -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: lake 2 | version: 0.1.4 3 | 4 | dependencies: 5 | iceberg: 6 | github: adlerhsieh/iceberg 7 | -------------------------------------------------------------------------------- /spec/_purge_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | # Cleaning files from previous specs 4 | # describe "Purge" do 5 | # it "all files" do 6 | # system("./lake -p") 7 | # File.directory?("#{ENV["PWD"]}/.lake").should be_false 8 | # File.file?("#{ENV["PWD"]}/Lakefile").should be_false 9 | # end 10 | # it "the executable" do 11 | # system("rm lake") 12 | # File.file?("#{ENV["PWD"]}/lake").should be_false 13 | # end 14 | # end 15 | system("rm Lakefile") 16 | system("rm lake") 17 | system("rm -rf .lake") 18 | system("rm create.txt") 19 | -------------------------------------------------------------------------------- /spec/build_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Lake" do 4 | it "builds lake executable" do 5 | system("crystal build ./src/lake.cr") 6 | File.file?("#{ENV["PWD"]}/lake").should be_true 7 | end 8 | 9 | it "without option and tasks" do 10 | system("./lake").should be_true 11 | File.file?("#{ENV["PWD"]}/Lakefile").should be_false 12 | File.directory?("#{ENV["PWD"]}/.lake").should be_false 13 | end 14 | 15 | it "creates tasks folder" do 16 | system("./lake -b").should be_true 17 | File.file?("#{ENV["PWD"]}/Lakefile").should be_true 18 | File.directory?("#{ENV["PWD"]}/.lake").should be_true 19 | end 20 | 21 | it "# creates tasks with helper" do 22 | File.write("#{ENV["PWD"]}/.lake/#{file_name[0]}.cr", task_content[0]) 23 | File.write("#{ENV["PWD"]}/.lake/#{file_name[1]}.cr", task_content[1]) 24 | File.file?("#{ENV["PWD"]}/.lake/#{file_name[0]}.cr").should be_true 25 | File.file?("#{ENV["PWD"]}/.lake/#{file_name[1]}.cr").should be_true 26 | end 27 | 28 | describe "-b" do 29 | it "builds tasks" do 30 | system("./lake -b") 31 | File.file?("#{ENV["PWD"]}/.lake/tasks/hello.cr").should be_true 32 | File.file?("#{ENV["PWD"]}/.lake/tasks/create.cr").should be_true 33 | File.file?("#{ENV["PWD"]}/.lake/tasks/go.cr").should be_true 34 | File.file?("#{ENV["PWD"]}/.lake/bin/hello").should be_true 35 | File.file?("#{ENV["PWD"]}/.lake/bin/create").should be_true 36 | File.file?("#{ENV["PWD"]}/.lake/bin/go").should be_true 37 | end 38 | 39 | it "deletes non-character symbols in task name" do 40 | File.file?("#{ENV["PWD"]}/.lake/tasks/hi-.cr").should be_false 41 | File.file?("#{ENV["PWD"]}/.lake/tasks/hi.cr").should be_true 42 | File.file?("#{ENV["PWD"]}/.lake/bin/hi").should be_true 43 | File.file?("#{ENV["PWD"]}/.lake/bin/hi-").should be_false 44 | end 45 | end 46 | end 47 | 48 | describe "options" do 49 | it "--help" do 50 | system("./lake -h").should be_true 51 | end 52 | it "--version" do 53 | system("./lake -v").should be_true 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/run_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Run tasks" do 4 | it "create" do 5 | system("./lake create") 6 | File.file?("#{ENV["PWD"]}/create.txt").should be_true 7 | end 8 | end 9 | 10 | describe "-g" do 11 | it "creates tasks in HOME" do 12 | system("mv -f ~/.lake ~/.lake-backup") if Dir.exists?("#{ENV["HOME"]}/.lake") 13 | system("mv ~/Lakefile ~/Lakefile-backup") if File.exists?("#{ENV["HOME"]}/Lakefile") 14 | system("mkdir ~/.lake") 15 | File.write("#{ENV["HOME"]}/.lake/#{file_name[0]}.cr", task_content[0]) 16 | Dir.exists?("#{ENV["HOME"]}/.lake").should be_true 17 | File.exists?("#{ENV["HOME"]}/.lake/#{file_name[0]}.cr").should be_true 18 | end 19 | 20 | it "run tasks from HOME" do 21 | system("ls ~/.lake") 22 | system("./lake -g hello").should be_true 23 | end 24 | 25 | it "remove testing files" do 26 | system("rm -rf ~/.lake") 27 | system("rm ~/Lakefile") 28 | system("mv -f ~/.lake-backup ~/.lake") if Dir.exists?("#{ENV["HOME"]}/.lake-backup") 29 | system("mv ~/Lakefile-backup ~/Lakefile") if File.exists?("#{ENV["HOME"]}/Lakefile-backup") 30 | Dir.exists?("#{ENV["HOME"]}/.lake-backup").should be_false 31 | File.exists?("#{ENV["HOME"]}/.Lakefile-backup").should be_false 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | # require "../src/lake" 3 | 4 | def file_name 5 | [ 6 | "hello", 7 | "go" 8 | ] 9 | end 10 | 11 | def task_content 12 | [ 13 | "Task.hello\n"\ 14 | "puts \"hello world\"\n"\ 15 | "Task.create\n"\ 16 | "system(\"touch create.txt\")\n"\ 17 | "Task.hi-\n"\ 18 | "puts \"hi\"", 19 | 20 | "Task.go\n"\ 21 | "require \"iceberg\"\n"\ 22 | "puts \"We need to go now.\"\n" 23 | ] 24 | end 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lake.cr: -------------------------------------------------------------------------------- 1 | require "./lake/*" 2 | require "colorize" 3 | require "option_parser" 4 | 5 | error = Lake::Exception.new 6 | finder = Lake::Finder.new 7 | 8 | OptionParser.parse! do |parser| 9 | parser.on("[taskname]", "Run specified tasks.") {} 10 | parser.on("-g", "--global", "Run specified tasks on global Lakefile.") { 11 | $home = true 12 | finder.change_root 13 | } 14 | parser.on("-b", "--build", "Build all tasks, ignoring existing tasks.") { 15 | finder.prepare 16 | finder.create_tasks 17 | exit 0 18 | } 19 | parser.on("-r", "--rebuild", "Rebuild all tasks.") { 20 | finder.prepare 21 | finder.recreate_tasks 22 | exit 0 23 | } 24 | parser.on("-h", "--help", "Shows this message.") { 25 | puts parser 26 | exit 0 27 | } 28 | parser.on("-v", "--version", "Display current version"){ 29 | puts Lake::VERSION 30 | exit 0 31 | } 32 | end 33 | 34 | if ARGV.size > 0 35 | runner = Lake::Runner.new(ARGV,$home) 36 | if runner.has_task? 37 | finder.prepare 38 | finder.create_tasks 39 | runner.tasks.each do |name| 40 | error.missing_task(name) unless finder.tasks.includes?(name) 41 | runner.run(name) 42 | end 43 | exit 0 44 | else 45 | error.no_task 46 | end 47 | end 48 | 49 | puts "No specified task." 50 | exit 0 51 | -------------------------------------------------------------------------------- /src/lake/builder.cr: -------------------------------------------------------------------------------- 1 | module Lake 2 | class Builder 3 | def initialize(@file) 4 | @content = File.read_lines(@file.to_s) 5 | @filename = @file.split("/")[-1] 6 | @root = @file.split("/")[0..-2].join("/") 7 | @root += "/.lake" if @filename == "Lakefile" 8 | end 9 | 10 | # Prepends Lake DSL to a task 11 | def prepend_dsl 12 | # WIP 13 | end 14 | 15 | # Matches task content with source 16 | def up_to_date? 17 | if File.file?(file(:tasks).to_s) 18 | @task == File.read(file(:tasks).to_s) 19 | else 20 | false 21 | end 22 | end 23 | 24 | def build_tasks 25 | tasks = {} of String? => MemoryIO | String? 26 | title = "" 27 | desc = [] of String 28 | @content.each_with_index do |line, index| 29 | if /^Task\./ =~ line 30 | title = line.match(/^Task\.(\w+)/) {|md| md[1] } 31 | else 32 | desc << line 33 | end 34 | # Ends a task collection at the last line of the task or file 35 | if index + 1 == @content.size || /^Task\./ =~ @content[index + 1] 36 | unless title == "" 37 | tasks[title] = desc.join("") 38 | end 39 | title = "" 40 | desc = [] of String 41 | end 42 | end 43 | crystal_build(tasks) 44 | end 45 | 46 | # Runs crystal build and output to the bin directory 47 | private def crystal_build(tasks) 48 | tasks.each do |task, content| 49 | task_name = "#{@root}/tasks/#{task}.cr" 50 | exe_name = "#{@root}/bin/#{task}" 51 | # Build task if the task isn't duplicate 52 | next if File.exists?(task_name) && File.read(task_name) == content 53 | File.write(task_name, content) 54 | system("crystal build #{task_name} -o #{exe_name}") 55 | if File.exists?(exe_name) 56 | puts "#{"Task built".colorize(:green)}: #{task}" 57 | else 58 | File.delete(task_name) 59 | puts "#{"Task ignored".colorize(:red)}: #{task}" 60 | end 61 | end 62 | end 63 | 64 | private def title_invalid(title) 65 | if /-/ =~ title 66 | Lake::Exception.new.taskname_incorrect 67 | next 68 | end 69 | end 70 | 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /src/lake/exception.cr: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | 3 | module Lake 4 | class Exception 5 | def initialize 6 | end 7 | 8 | def taskname_incorrect 9 | puts "\"-\" in a task name is not allowed." 10 | end 11 | 12 | def missing_lakefile 13 | abort("#{error}: Couldn't find \"Lakefile\"") 14 | end 15 | 16 | def missing_task(name) 17 | abort("#{error}: Couldn't find task \"#{name}\"") 18 | end 19 | 20 | def no_task 21 | abort("No task found.") 22 | end 23 | 24 | private def error 25 | "Error".colorize(:red) 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/lake/finder.cr: -------------------------------------------------------------------------------- 1 | module Lake 2 | class Finder 3 | getter :lakefile 4 | property :bin 5 | property :root 6 | def initialize 7 | @root = ENV["PWD"] 8 | @lakefile = find_lakefile.to_s 9 | end 10 | 11 | def find_lakefile 12 | if File.file?("#{@root}/Lakefile") 13 | return "#{@root}/Lakefile" 14 | end 15 | end 16 | 17 | def find_lake_dir 18 | if File.directory?("#{@root}/.lake") 19 | return "#{@root}/.lake" 20 | end 21 | end 22 | 23 | def tasks 24 | Dir.entries("#{@root}/.lake/bin").map {|file| 25 | file if File.file?("#{@root}/.lake/bin/#{file}") 26 | }.compact.join(", ") 27 | end 28 | 29 | def prepare 30 | unless File.file?("#{@root}/Lakefile") 31 | system("touch #{@root}/Lakefile") 32 | puts "#{"Created".colorize(:green)}: #{@root}/Lakefile" 33 | end 34 | unless Dir.exists?("#{@root}/.lake") 35 | Dir.mkdir("#{@root}/.lake") 36 | puts "#{"Created".colorize(:green)}: #{@root}/.lake" 37 | end 38 | unless Dir.exists?("#{@root}/.lake/tasks") 39 | Dir.mkdir("#{@root}/.lake/tasks") 40 | puts "#{"Created".colorize(:green)}: #{@root}/.lake/bin" 41 | end 42 | unless Dir.exists?("#{@root}/.lake/bin") 43 | Dir.mkdir("#{@root}/.lake/bin") 44 | puts "#{"Created".colorize(:green)}: #{@root}/.lake/tasks" 45 | end 46 | end 47 | 48 | def create_tasks 49 | Dir.entries("#{@root}/.lake").each do |file| 50 | is_file = File.file?("#{@root}/.lake/#{file}") 51 | is_cr = File.extname(file) == ".cr" 52 | next unless is_file && is_cr 53 | Lake::Builder.new("#{@root}/.lake/#{file}").build_tasks 54 | end 55 | Lake::Builder.new(find_lakefile.to_s).build_tasks 56 | end 57 | 58 | def remove(type) 59 | Dir.entries("#{@root}/.lake/#{type}").each do |file| 60 | filepath = "#{@root}/.lake/#{type}/#{file}" 61 | File.delete(filepath) if File.file?(filepath) 62 | end 63 | end 64 | 65 | def recreate_tasks 66 | remove("tasks") 67 | remove("bin") 68 | create_tasks 69 | end 70 | 71 | def change_root 72 | @root = ENV["HOME"] 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/lake/runner.cr: -------------------------------------------------------------------------------- 1 | module Lake 2 | class Runner 3 | getter tasks 4 | 5 | def initialize(args : Array(String), home = false) 6 | @root = if home 7 | @root = ENV["HOME"] 8 | else 9 | @root = ENV["PWD"] 10 | end 11 | @tasks = args.map {|arg| arg.includes?("-") ? nil : arg }.compact 12 | end 13 | 14 | def run(filename) 15 | system("#{@root}/.lake/bin/#{filename}") 16 | end 17 | 18 | def has_task? 19 | @tasks.size > 0 20 | end 21 | 22 | def change_root 23 | @root = ENV["HOME"] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/lake/version.cr: -------------------------------------------------------------------------------- 1 | module Lake 2 | VERSION = "0.2.0" 3 | end 4 | --------------------------------------------------------------------------------