├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── Rakefile ├── bintest └── mruby-cli.rb ├── build_config.rb ├── circle.yml ├── docker-compose.yml ├── mrbgem.rake ├── mrblib ├── mruby-cli.rb └── mruby-cli │ ├── cli.rb │ ├── help.rb │ ├── option.rb │ ├── options.rb │ ├── setup.rb │ ├── util.rb │ └── version.rb ├── test └── mruby-cli │ ├── test_option.rb │ ├── test_options.rb │ └── test_util.rb └── tools └── mruby-cli └── mruby-cli.c /.gitignore: -------------------------------------------------------------------------------- 1 | mruby/ 2 | releases/ 3 | packages/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: required 3 | services: 4 | - docker 5 | before_install: 6 | - sudo apt-get -qq update 7 | install: 8 | - sudo sh -c "curl -L https://github.com/docker/compose/releases/download/1.3.3/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose" 9 | - sudo chmod +x /usr/local/bin/docker-compose 10 | script: 11 | - docker-compose run test 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.2 08-21-2015 2 | * improve development documentation: #9 3 | * remove extraneous `mruby/mruby/` dir: ab8ae96 4 | * generated mtest actually passes now: 53eacfb 5 | * generated 64-bit linux binaries are now built in `mruby/build/x64_64-pc-linux-gnu/bin/`: #14 6 | * generated host now has debug on by default for stack traces: #14 7 | 8 | # 0.0.1 07-27-2015 9 | * First Release! 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hone/mruby-cli 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Terence Lee, Zachary Scott 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 | # mruby CLI 2 | A utility for setting up a CLI with [mruby](https://www.mruby.org) that compiles binaries to Linux, OS X, and Windows. 3 | 4 | ## Prerequisites 5 | You'll need the following installed and in your `PATH`: 6 | 7 | * [mruby-cli](https://github.com/hone/mruby-cli/releases) 8 | * [Docker](https://docs.docker.com/installation/) 9 | * [Docker Compose](https://docs.docker.com/compose/install/) 10 | 11 | On Mac OS X and Windows, [Docker Toolbox](https://www.docker.com/toolbox) is the recommended way to install Docker and docker-compose (does not work on windows). 12 | 13 | ## Building a CLI app 14 | To generate a new mruby CLI, there's a `--setup` option. 15 | 16 | ```sh 17 | $ mruby-cli --setup 18 | ``` 19 | 20 | This will generate a folder `` containing a basic skeleton for getting started. Once you're in the folder, you can build all the binaries: 21 | 22 | ```sh 23 | $ docker-compose run compile 24 | ``` 25 | 26 | You'll be able to find the binaries in the following directories: 27 | 28 | * Linux (64-bit): `mruby/build/x86_64-pc-linux-gnu/bin` 29 | * Linux (32-bit): `mruby/build/i686-pc-linux-gnu/bin` 30 | * OS X (64-bit): `mruby/build/x86_64-apple-darwin14/bin/` 31 | * OS X (32-bit): `mruby/build/i386-apple-darwin14/bin` 32 | * Windows (64-bit): `mruby/build/x86_64-w64-mingw32/bin/` 33 | * Windows (32-bit): `mruby/build/i686-w64-mingw32/bin` 34 | 35 | You should be able to run the respective binary that's native on your platform. There's a `shell` service that can be used as well. In the example below, `mruby-cli --setup hello_world` was run. 36 | 37 | ```sh 38 | $ docker-compose run shell 39 | root@3da278e931fc:/home/mruby/code# mruby/build/host/bin/hello_world 40 | Hello World 41 | ``` 42 | 43 | ### On Windows system 44 | 45 | When running on a windows system `docker-compose run`, you need to add the flag 46 | `-d`. For instance, `docker-compose run -d compile`. If you don't add it, you 47 | will got the following error: 48 | 49 | ``` 50 | [31mERROR 51 | Please pass the -d flag when using `docker-compose run`. 52 | ``` 53 | 54 | ## Docker 55 | 56 | Each app will be generated with a Dockerfile that inherits a base image. 57 | 58 | You can pull the image from docker hub here: 59 | https://registry.hub.docker.com/u/hone/mruby-cli/ 60 | 61 | The Dockerfile for the base image is available on github: 62 | https://github.com/hone/mruby-cli-docker 63 | 64 | ## Hello World 65 | 66 | Building the canonical hello world example in mruby-cli is quite simple. The two files of note from the generate skeleton are `mrblib/hello_world.rb` and `mrbgem.rake`. The CLI hooks into the `__main__` method defined here and passes all the arguments as `argv`. 67 | 68 | `mrblib/hello_world.rb`: 69 | ```ruby 70 | def __main__(argv) 71 | puts "Hello World" 72 | end 73 | ``` 74 | 75 | ### Dependencies 76 | The rubygems equivalent is mrbgems. [mgem-list](https://github.com/mruby/mgem-list) contains a list of mgems you can pull from. By default mruby does not include everything in the kitchen sink like MRI. This means to even get `puts`, we need to include the `mruby-print`. The list of core gems can be found [here](https://github.com/mruby/mruby/tree/master/mrbgems). Adding dependencies is simple, you just need to add a line near the bottom of your `mrbgem.rake` with the two arguments: name and where it comes from. 77 | 78 | `mrbgem.rake`: 79 | ```ruby 80 | MRuby::Gem::Specification.new('hello_world') do |spec| 81 | spec.license = 'MIT' 82 | spec.author = 'Terence Lee' 83 | spec.summary = 'Hello World' 84 | spec.bins = ['hello_world'] 85 | 86 | spec.add_dependency 'mruby-print', :core => 'mruby-print' 87 | spec.add_dependency 'mruby-mtest', :mgem => 'mruby-test' 88 | end 89 | ``` 90 | ### CLI Architecture 91 | The app is built from two parts a C wrapper in `tools/` and a mruby part in `mrblib/`. The C wrapper is fairly minimal and executes the `__main__` method in mruby and instantiates `ARGV` and passes it to the mruby code. You won't need to touch the C wrapper. The rest of the CLI is written in mruby. You can't have subfolders in `mrblib/` but you can have as many files in `mrblib/`. All these files are precompiled into mruby bytecode The build tool for mruby is written in CRuby (MRI). 92 | 93 | ### Testing 94 | By default, `mruby-cli` generates two kinds of tests: mtest and bintest. 95 | 96 | #### mtest 97 | These tests are unit tests, are written in mruby, and go in the `test/` directory. It uses the mrbgem [`mruby-mtest`](https://github.com/iij/mruby-mtest). The available methods to be used can be found [here](https://github.com/mruby/mruby/blob/master/test/assert.rb). To run the tests, just execute: 98 | 99 | ```sh 100 | $ docker-compose run mtest 101 | ``` 102 | 103 | #### bintest 104 | These are integration tests, are written in CRuby (MRI), and go in the `bintest/` directory. It tests the status and output of the host binary inside a docker container. To run them just execute: 105 | 106 | ```sh 107 | $ docker-compose run bintest 108 | ``` 109 | 110 | ## Examples 111 | * `mruby-cli` itself is an app generated by `mruby-cli`, so you can explore this repo on how to build one. 112 | * [mjruby](https://github.com/jkutner/mjruby) - replacement for jruby-launcher. 113 | * [mruby-eso-research](https://github.com/hone/mruby-eso-research) - an app for managing crafting research in Elder Scrolls Online. It uses YAML as the data store. 114 | * [nhk-easy-cli](https://github.com/nhk-ruby/nhk-easy-cli) - a command-line client for reading NHK News Web Easy. 115 | * [mruby-static](https://github.com/zzak/mruby-static) - a static site generator 116 | 117 | ## mruby-cli Development 118 | 119 | ### Compile the mruby-cli binaries 120 | 121 | This app is built as a `mruby-cli` app. To compile the binaries, you **must** type 122 | 123 | ``` 124 | docker-compose run compile 125 | ``` 126 | 127 | and find the binaries in the appropriate directories (`mruby/build//bin/`). 128 | 129 | The docker container contains the necessary cross toolchain to compile a binary for each supported target. That's why it is checked before running a rake task if it is run inside a container. 130 | 131 | Indeed, just using `rake compile` will not work out of the box because the main build is designed to compile on a 64-bit Linux host. It could work if you are on a 64-Linux host and you have an cross toolchain equivalent to the one we provide into the docker container. 132 | 133 | This means that if you want to add a new rake task `my_task`, you need to add it to the `docker-compose.yml` to make it available through `docker-compose run my_task`. 134 | 135 | ### Create the releases 136 | 137 | Just type: `docker-compose run release` 138 | 139 | After this command finishes, you'll see the releases for each target in the `releases` directory. 140 | 141 | ### Create package 142 | 143 | We can package the ad hoc release as deb, rpm, msi, or dmg for the following 144 | Linux. 145 | 146 | To create all the package, just type 147 | 148 | ``` 149 | docker-compose run package 150 | ``` 151 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | MRUBY_VERSION="1.2.0" 4 | 5 | file :mruby do 6 | #sh "git clone --depth=1 https://github.com/mruby/mruby" 7 | sh "curl -L --fail --retry 3 --retry-delay 1 https://github.com/mruby/mruby/archive/#{MRUBY_VERSION}.tar.gz -s -o - | tar zxf -" 8 | FileUtils.mv("mruby-#{MRUBY_VERSION}", "mruby") 9 | end 10 | 11 | APP_NAME=ENV["APP_NAME"] || "mruby-cli" 12 | APP_ROOT=ENV["APP_ROOT"] || Dir.pwd 13 | # avoid redefining constants in mruby Rakefile 14 | mruby_root=File.expand_path(ENV["MRUBY_ROOT"] || "#{APP_ROOT}/mruby") 15 | mruby_config=File.expand_path(ENV["MRUBY_CONFIG"] || "build_config.rb") 16 | ENV['MRUBY_ROOT'] = mruby_root 17 | ENV['MRUBY_CONFIG'] = mruby_config 18 | Rake::Task[:mruby].invoke unless Dir.exist?(mruby_root) 19 | load "#{mruby_root}/Rakefile" 20 | 21 | load File.join(File.expand_path(File.dirname(__FILE__)), "mrbgem.rake") 22 | 23 | current_gem = MRuby::Gem.current 24 | app_version = MRuby::Gem.current.version 25 | APP_VERSION = (app_version.nil? || app_version.empty?) ? "unknown" : app_version 26 | 27 | desc "compile all the binaries" 28 | task :compile => [:all] do 29 | Dir.chdir(mruby_root) do 30 | MRuby.each_target do |target| 31 | `#{target.cc.command} --version` 32 | abort("Command #{target.cc.command} for #{target.name} is missing.") unless $?.success? 33 | end 34 | %W(#{mruby_root}/build/x86_64-pc-linux-gnu/bin/#{APP_NAME} #{mruby_root}/build/i686-pc-linux-gnu/#{APP_NAME}).each do |bin| 35 | sh "strip --strip-unneeded #{bin}" if File.exist?(bin) 36 | end 37 | end 38 | end 39 | 40 | namespace :test do 41 | desc "run mruby & unit tests" 42 | # only build mtest for host 43 | task :mtest => :compile do 44 | Dir.chdir(mruby_root) do 45 | # in order to get mruby/test/t/synatx.rb __FILE__ to pass, 46 | # we need to make sure the tests are built relative from mruby_root 47 | MRuby.each_target do |target| 48 | # only run unit tests here 49 | target.enable_bintest = false 50 | run_test if target.test_enabled? 51 | end 52 | end 53 | end 54 | 55 | def clean_env(envs) 56 | old_env = {} 57 | envs.each do |key| 58 | old_env[key] = ENV[key] 59 | ENV[key] = nil 60 | end 61 | yield 62 | envs.each do |key| 63 | ENV[key] = old_env[key] 64 | end 65 | end 66 | 67 | desc "run integration tests" 68 | task :bintest => :compile do 69 | Dir.chdir(mruby_root) do 70 | MRuby.each_target do |target| 71 | clean_env(%w(MRUBY_ROOT MRUBY_CONFIG)) do 72 | run_bintest if target.bintest_enabled? 73 | end 74 | end 75 | end 76 | end 77 | end 78 | 79 | desc "run all tests" 80 | Rake::Task['test'].clear 81 | task :test => ['test:bintest', 'test:mtest'] 82 | 83 | desc "cleanup" 84 | task :clean do 85 | Dir.chdir(mruby_root) do 86 | sh "rake deep_clean" 87 | end 88 | end 89 | 90 | desc "generate a release tarball" 91 | task :release => :compile do 92 | require 'tmpdir' 93 | 94 | Dir.chdir(mruby_root) do 95 | # since we're in the mruby/ 96 | release_dir = "releases/v#{APP_VERSION}" 97 | release_path = Dir.pwd + "/../#{release_dir}" 98 | app_name = "#{APP_NAME}-#{APP_VERSION}" 99 | FileUtils.mkdir_p(release_path) 100 | 101 | Dir.mktmpdir do |tmp_dir| 102 | Dir.chdir(tmp_dir) do 103 | MRuby.each_target do |target| 104 | next if name == "host" 105 | 106 | arch = name 107 | bin = "#{build_dir}/bin/#{exefile(APP_NAME)}" 108 | FileUtils.mkdir_p(name) 109 | FileUtils.cp(bin, name) 110 | 111 | Dir.chdir(arch) do 112 | arch_release = "#{app_name}-#{arch}" 113 | puts "Writing #{release_dir}/#{arch_release}.tgz" 114 | `tar czf #{release_path}/#{arch_release}.tgz *` 115 | end 116 | end 117 | 118 | puts "Writing #{release_dir}/#{app_name}.tgz" 119 | `tar czf #{release_path}/#{app_name}.tgz *` 120 | end 121 | end 122 | end 123 | end 124 | 125 | namespace :local do 126 | desc "show version" 127 | task :version do 128 | puts "#{APP_NAME} #{APP_VERSION}" 129 | end 130 | end 131 | 132 | def is_in_a_docker_container? 133 | `grep -q docker /proc/self/cgroup` 134 | $?.success? 135 | end 136 | 137 | Rake.application.tasks.each do |task| 138 | next if ENV["MRUBY_CLI_LOCAL"] 139 | unless task.name.start_with?("local:") 140 | # Inspired by rake-hooks 141 | # https://github.com/guillermo/rake-hooks 142 | old_task = Rake.application.instance_variable_get('@tasks').delete(task.name) 143 | desc old_task.full_comment 144 | task old_task.name => old_task.prerequisites do 145 | abort("Not running in docker, you should type \"docker-compose run \".") \ 146 | unless is_in_a_docker_container? 147 | old_task.invoke 148 | end 149 | end 150 | end 151 | 152 | namespace :package do 153 | require 'fileutils' 154 | require 'tmpdir' 155 | 156 | version = APP_VERSION 157 | release_dir = "releases/v#{version}" 158 | package_dir = "packages/v#{version}" 159 | release_path = Dir.pwd + "/../#{release_dir}" 160 | package_path = Dir.pwd + "/../#{package_dir}" 161 | FileUtils.mkdir_p(package_path) 162 | 163 | def check_fpm_installed? 164 | `gem list -i fpm`.chomp == "true" 165 | end 166 | 167 | def check_msi_installed? 168 | `wixl --version` 169 | $?.success? 170 | end 171 | 172 | def check_dmg_installed? 173 | `genisoimage --version` 174 | $?.success? 175 | end 176 | 177 | def wxs_content(version, arch) 178 | arch_wxs = case arch 179 | when "x86_64" 180 | { 181 | string: "64-bit", 182 | program_files_folder: "ProgramFiles64Folder", 183 | define: "" 184 | } 185 | else 186 | { 187 | string: "32-bit", 188 | program_files_folder: "ProgramFilesFolder", 189 | define: "" 190 | } 191 | end 192 | 193 | <<-EOF 194 | 195 | 196 | 197 | #{arch_wxs[:define]} 198 | 199 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | EOF 228 | end 229 | 230 | def info_plist_content(version, arch) 231 | <<-EOF 232 | 233 | 234 | 235 | 236 | CFBundleExecutable 237 | mruby-cli 238 | CFBundleGetInfoString 239 | mruby-cli #{version} #{arch} 240 | CFBundleName 241 | mruby-cli 242 | CFBundleIdentifier 243 | mruby-cli 244 | CFBundlePackageType 245 | APPL 246 | CFBundleShortVersionString 247 | #{version} 248 | CFBundleSignature 249 | mrbc 250 | CFBundleInfoDictionaryVersion 251 | 6.0 252 | 253 | 254 | EOF 255 | end 256 | 257 | def osx_setup_bash_path_script 258 | <<-EOF 259 | #!/bin/bash 260 | echo "export PATH=$PATH:/Applications/mruby-cli.app/Contents/MacOs" >> $HOME/.bash_profile 261 | source $HOME/.bash_profile 262 | EOF 263 | end 264 | 265 | def log(package_dir, version, package) 266 | puts "Writing packages #{package_dir}/#{version}/#{package}" 267 | end 268 | 269 | desc "create deb package" 270 | task :deb => [:release] do 271 | abort("fpm is not installed. Please check your docker install.") unless check_fpm_installed? 272 | 273 | ["x86_64", "i686"].each do |arch| 274 | release_tar_file = "mruby-cli-#{version}-#{arch}-pc-linux-gnu.tgz" 275 | arch_name = (arch == "x86_64" ? "amd64" : arch) 276 | log(package_dir, version, "mruby-cli_#{version}_#{arch_name}.deb") 277 | `fpm -s tar -t deb -a #{arch} -n mruby-cli -v #{version} --prefix /usr/bin -p #{package_path} #{release_path}/#{release_tar_file}` 278 | end 279 | end 280 | 281 | desc "create rpm package" 282 | task :rpm => [:release] do 283 | abort("fpm is not installed. Please check your docker install.") unless check_fpm_installed? 284 | 285 | ["x86_64", "i686"].each do |arch| 286 | release_tar_file = "mruby-cli-#{version}-#{arch}-pc-linux-gnu.tgz" 287 | log(package_dir, version, "mruby-cli-#{version}-1.#{arch}.rpm") 288 | `fpm -s tar -t rpm -a #{arch} -n mruby-cli -v #{version} --prefix /usr/bin -p #{package_path} #{release_path}/#{release_tar_file}` 289 | end 290 | end 291 | 292 | desc "create msi package" 293 | task :msi => [:release] do 294 | abort("msitools is not installed. Please check your docker install.") unless check_msi_installed? 295 | ["x86_64", "i686"].each do |arch| 296 | log(package_dir, version, "mruby-cli-#{version}-#{arch}.msi") 297 | release_tar_file = "mruby-cli-#{version}-#{arch}-w64-mingw32.tgz" 298 | Dir.mktmpdir do |dest_dir| 299 | Dir.chdir dest_dir 300 | `tar -zxf #{release_path}/#{release_tar_file}` 301 | File.write("mruby-cli-#{version}-#{arch}.wxs", wxs_content(version, arch)) 302 | `wixl -v mruby-cli-#{version}-#{arch}.wxs && mv mruby-cli-#{version}-#{arch}.msi #{package_path}` 303 | end 304 | end 305 | end 306 | 307 | desc "create dmg package" 308 | task :dmg => [:release] do 309 | abort("dmg tools are not installed. Please check your docker install.") unless check_dmg_installed? 310 | ["x86_64", "i386"].each do |arch| 311 | log(package_dir, version, "mruby-cli-#{version}-#{arch}.dmg") 312 | release_tar_file = "mruby-cli-#{version}-#{arch}-apple-darwin14.tgz" 313 | Dir.mktmpdir do |dest_dir| 314 | Dir.chdir dest_dir 315 | `tar -zxf #{release_path}/#{release_tar_file}` 316 | FileUtils.chmod 0755, "mruby-cli" 317 | FileUtils.mkdir_p "mruby-cli.app/Contents/MacOs" 318 | FileUtils.mv "mruby-cli", "mruby-cli.app/Contents/MacOs" 319 | File.write("mruby-cli.app/Contents/Info.plist", info_plist_content(version, arch)) 320 | File.write("add-mruby-cli-to-my-path.sh", osx_setup_bash_path_script) 321 | FileUtils.chmod 0755, "add-mruby-cli-to-my-path.sh" 322 | `genisoimage -V mruby-cli -D -r -apple -no-pad -o #{package_path}/mruby-cli-#{version}-#{arch}.dmg #{dest_dir}` 323 | end 324 | end 325 | end 326 | 327 | end 328 | 329 | desc "create all packages" 330 | task :package => ["package:deb", "package:rpm", "package:msi", "package:dmg"] 331 | 332 | -------------------------------------------------------------------------------- /bintest/mruby-cli.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'tmpdir' 3 | 4 | BIN_PATH = File.join(File.dirname(__FILE__), "../mruby/bin/mruby-cli") 5 | 6 | assert('setup') do 7 | Dir.mktmpdir do |tmp_dir| 8 | Dir.chdir(tmp_dir) do 9 | app_name = "new_cli" 10 | output, status = Open3.capture2(BIN_PATH, "--setup", app_name) 11 | 12 | assert_true status.success?, "Process did not exit cleanly" 13 | assert_true Dir.exist?(app_name) 14 | Dir.chdir(app_name) do 15 | (%w(.gitignore mrbgem.rake build_config.rb Rakefile Dockerfile docker-compose.yml) + ["tools/#{app_name}/#{app_name}.c", "mrblib/#{app_name}.rb", "bintest/#{app_name}.rb"]).each do |file| 16 | assert_true(File.exist?(file), "Could not find #{file}") 17 | assert_include output, " create #{file}" 18 | end 19 | end 20 | end 21 | end 22 | end 23 | 24 | assert('setup can compile and run the generated app') do 25 | Dir.mktmpdir do |tmp_dir| 26 | Dir.chdir(tmp_dir) do 27 | app_name = "hello_world" 28 | APP_PATH = File.join("mruby/bin/#{app_name}") 29 | Open3.capture2(BIN_PATH, "--setup", app_name) 30 | 31 | Dir.chdir(app_name) do 32 | output, status = Open3.capture2("rake compile") 33 | assert_true status.success?, "`rake compile` did not exit cleanly" 34 | 35 | output, status = Open3.capture2(APP_PATH) 36 | assert_true status.success?, "`#{app_name}` did not exit cleanly" 37 | assert_include output, "Hello World" 38 | 39 | %w(x86_64-pc-linux-gnu i686-pc-linux-gnu).each do |host| 40 | output, status = Open3.capture2("file mruby/build/x86_64-pc-linux-gnu/bin/#{app_name}") 41 | assert_include output, ", stripped" 42 | end 43 | 44 | output, status = Open3.capture2("rake test:bintest") 45 | assert_true status.success?, "`rake test:bintest` did not exit cleanly" 46 | 47 | output, status = Open3.capture2("rake test:mtest") 48 | assert_true status.success?, "`rake test:mtest` did not exit cleanly" 49 | assert_false output.include?("Error:"), "mtest has errors" 50 | assert_false output.include?("Failure:"), "mtest has failures" 51 | end 52 | end 53 | end 54 | end 55 | 56 | assert('version') do 57 | require_relative '../mrblib/mruby-cli/version' 58 | output, status = Open3.capture2(BIN_PATH, "--version") 59 | assert_true status.success?, "Process did not exit cleanly" 60 | assert_include output, "mruby-cli version #{MRubyCLI::Version::VERSION}" 61 | end 62 | 63 | assert('help') do 64 | output, status = Open3.capture2(BIN_PATH, "--help") 65 | assert_true status.success?, "Process did not exit cleanly" 66 | assert_include output, "mruby-cli [switches] [arguments]" 67 | end 68 | -------------------------------------------------------------------------------- /build_config.rb: -------------------------------------------------------------------------------- 1 | def gem_config(conf) 2 | #conf.gembox 'default' 3 | 4 | # be sure to include this gem 5 | conf.gem File.expand_path(File.dirname(__FILE__)) 6 | end 7 | 8 | MRuby::Build.new do |conf| 9 | toolchain :gcc 10 | 11 | conf.enable_bintest 12 | conf.enable_debug 13 | conf.enable_test 14 | 15 | gem_config(conf) 16 | end 17 | 18 | MRuby::Build.new('x86_64-pc-linux-gnu') do |conf| 19 | toolchain :gcc 20 | 21 | gem_config(conf) 22 | end 23 | 24 | MRuby::CrossBuild.new('i686-pc-linux-gnu') do |conf| 25 | toolchain :gcc 26 | 27 | [conf.cc, conf.cxx, conf.linker].each do |cc| 28 | cc.flags << "-m32" 29 | end 30 | 31 | gem_config(conf) 32 | end 33 | 34 | MRuby::CrossBuild.new('x86_64-apple-darwin14') do |conf| 35 | toolchain :clang 36 | 37 | [conf.cc, conf.linker].each do |cc| 38 | cc.command = 'x86_64-apple-darwin14-clang' 39 | end 40 | conf.cxx.command = 'x86_64-apple-darwin14-clang++' 41 | conf.archiver.command = 'x86_64-apple-darwin14-ar' 42 | 43 | conf.build_target = 'x86_64-pc-linux-gnu' 44 | conf.host_target = 'x86_64-apple-darwin14' 45 | 46 | gem_config(conf) 47 | end 48 | 49 | MRuby::CrossBuild.new('i386-apple-darwin14') do |conf| 50 | toolchain :clang 51 | 52 | [conf.cc, conf.linker].each do |cc| 53 | cc.command = 'i386-apple-darwin14-clang' 54 | end 55 | conf.cxx.command = 'i386-apple-darwin14-clang++' 56 | conf.archiver.command = 'i386-apple-darwin14-ar' 57 | 58 | conf.build_target = 'i386-pc-linux-gnu' 59 | conf.host_target = 'i386-apple-darwin14' 60 | 61 | gem_config(conf) 62 | end 63 | 64 | MRuby::CrossBuild.new('x86_64-w64-mingw32') do |conf| 65 | toolchain :gcc 66 | 67 | [conf.cc, conf.linker].each do |cc| 68 | cc.command = 'x86_64-w64-mingw32-gcc' 69 | end 70 | conf.cxx.command = 'x86_64-w64-mingw32-g++' 71 | conf.archiver.command = 'x86_64-w64-mingw32-gcc-ar' 72 | 73 | conf.exts do |exts| 74 | exts.object = '.obj' 75 | exts.executable = '.exe' 76 | exts.library = '.lib' 77 | end 78 | 79 | conf.build_target = 'x86_64-pc-linux-gnu' 80 | conf.host_target = 'x86_64-w64-mingw32' 81 | 82 | gem_config(conf) 83 | end 84 | 85 | MRuby::CrossBuild.new('i686-w64-mingw32') do |conf| 86 | toolchain :gcc 87 | 88 | [conf.cc, conf.linker].each do |cc| 89 | cc.command = 'i686-w64-mingw32-gcc' 90 | end 91 | conf.cxx.command = 'i686-w64-mingw32-g++' 92 | conf.archiver.command = 'i686-w64-mingw32-gcc-ar' 93 | 94 | conf.exts do |exts| 95 | exts.object = '.obj' 96 | exts.executable = '.exe' 97 | exts.library = '.lib' 98 | end 99 | 100 | conf.build_target = 'i686-pc-linux-gnu' 101 | conf.host_target = 'i686-w64-mingw32' 102 | 103 | gem_config(conf) 104 | end 105 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | executorType: machine 3 | 4 | stages: 5 | build: 6 | workDir: ~/mruby-cli 7 | steps: 8 | - type: checkout 9 | - type: shell 10 | name: Run Tests 11 | command: sudo docker-compose build && sudo docker-compose run test 12 | 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | compile: &defaults 2 | build: . 3 | volumes: 4 | - .:/home/mruby/code:rw 5 | command: rake compile 6 | test: 7 | <<: *defaults 8 | command: rake test 9 | bintest: 10 | <<: *defaults 11 | command: rake test:bintest 12 | mtest: 13 | <<: *defaults 14 | command: rake test:mtest 15 | clean: 16 | <<: *defaults 17 | command: rake clean 18 | shell: 19 | <<: *defaults 20 | command: bash 21 | release: 22 | <<: *defaults 23 | command: rake release 24 | package: 25 | <<: *defaults 26 | command: rake package 27 | -------------------------------------------------------------------------------- /mrbgem.rake: -------------------------------------------------------------------------------- 1 | require_relative 'mrblib/mruby-cli/version' 2 | 3 | spec = MRuby::Gem::Specification.new('mruby-cli') do |spec| 4 | spec.bins = ['mruby-cli'] 5 | spec.add_dependency 'mruby-io', :mgem => 'mruby-io' 6 | spec.add_dependency 'mruby-getopts', :mgem => 'mruby-getopts' 7 | spec.add_dependency 'mruby-dir', :mgem => 'mruby-dir' 8 | spec.add_dependency 'mruby-mtest', :mgem => 'mruby-mtest' 9 | end 10 | 11 | spec.license = 'MIT' 12 | spec.authors = ['Terence Lee', 'Zachary Scott'] 13 | spec.summary = 'mruby cli utility' 14 | spec.version = MRubyCLI::Version::VERSION 15 | 16 | -------------------------------------------------------------------------------- /mrblib/mruby-cli.rb: -------------------------------------------------------------------------------- 1 | def __main__(argv) 2 | MRubyCLI::CLI.new(argv).run 3 | end 4 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/cli.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class CLI 3 | def initialize(argv, output_io = $stdout, error_io = $stderr) 4 | @options = setup_options 5 | @opts = @options.parse(argv) 6 | @output_io = output_io 7 | @error_io = error_io 8 | end 9 | 10 | def run 11 | if app_name = @options.option(:setup) 12 | Setup.new(app_name, @output_io).run 13 | elsif @options.option(:version) 14 | Version.new(@output_io).run 15 | else 16 | Help.new(@output_io).run 17 | end 18 | end 19 | 20 | private 21 | def setup_options 22 | options = Options.new 23 | options.add(Option.new("setup", "s", true)) 24 | options.add(Option.new("version", "v")) 25 | options.add(Option.new("help", "h")) 26 | 27 | options 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/help.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class Help 3 | def initialize(output_io) 4 | @output_io = output_io 5 | end 6 | 7 | def run 8 | @output_io.puts "mruby-cli [switches] [arguments]" 9 | @output_io.puts "mruby-cli -h, --help : show this message" 10 | @output_io.puts "mruby-cli -s, --setup= : setup your app" 11 | @output_io.puts "mruby-cli -v, --version : print mruby-cli version" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/option.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class Option 3 | attr_reader :short, :long, :value 4 | 5 | def initialize(long, short, value = false) 6 | @short = short 7 | @long = long 8 | @value = value 9 | end 10 | 11 | def to_long_opt 12 | to_getopt(@long, @value) 13 | end 14 | 15 | def to_short_opt 16 | to_getopt(@short, @value) 17 | end 18 | 19 | private 20 | def to_getopt(name, value) 21 | value ? "#{name}:" : name 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/options.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class Options 3 | attr_reader :short_opts, :long_opts 4 | attr_writer :parsed_opts 5 | 6 | def initialize 7 | @options = {} 8 | @short_opts_array = [] 9 | @short_opts = "" 10 | @long_opts = [] 11 | @parsed_opts = {} 12 | end 13 | 14 | def add(option) 15 | @options[option.long.to_sym] = option 16 | @long_opts << option.to_long_opt 17 | @long_opts.sort! 18 | @short_opts_array << option.to_short_opt 19 | @short_opts = @short_opts_array.sort!.join("") 20 | 21 | option 22 | end 23 | 24 | def parse(args) 25 | class << args; include Getopts; end 26 | @parsed_opts = args.getopts(@short_opts, *@long_opts) 27 | end 28 | 29 | def option(long_opt) 30 | option = @options[long_opt] 31 | 32 | return nil unless option 33 | if retn = @parsed_opts[option.long] 34 | if option.value 35 | return retn unless retn.empty? 36 | else 37 | return retn 38 | end 39 | end 40 | return @parsed_opts[option.short] if @parsed_opts[option.short] 41 | return false 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/setup.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class Setup 3 | def initialize(name, output) 4 | @name = name 5 | @output = output 6 | end 7 | 8 | def run 9 | Dir.mkdir(@name) unless Dir.exist?(@name) 10 | Dir.chdir(@name) do 11 | write_file(".gitignore", gitignore) 12 | write_file("mrbgem.rake", mrbgem_rake) 13 | write_file("build_config.rb", build_config_rb) 14 | write_file("Rakefile", rakefile) 15 | write_file("Dockerfile", dockerfile) 16 | write_file("docker-compose.yml", docker_compose_yml) 17 | 18 | create_dir_p("tools/#{@name}") 19 | write_file("tools/#{@name}/#{@name}.c", tools) 20 | 21 | create_dir("mrblib") 22 | write_file("mrblib/#{@name}.rb", mrblib) 23 | 24 | create_dir("mrblib/#{@name}") 25 | write_file("mrblib/#{@name}/version.rb", version) 26 | 27 | create_dir("bintest") 28 | write_file("bintest/#{@name}.rb", bintest) 29 | 30 | create_dir("test") 31 | write_file("test/test_#{@name}.rb", test) 32 | end 33 | end 34 | 35 | private 36 | def create_dir_p(dir) 37 | dir.split("/").inject("") do |parent, base| 38 | new_dir = 39 | if parent == "" 40 | base 41 | else 42 | "#{parent}/#{base}" 43 | end 44 | 45 | create_dir(new_dir) 46 | 47 | new_dir 48 | end 49 | end 50 | 51 | def create_dir(dir) 52 | if Dir.exist?(dir) 53 | @output.puts " skip #{dir}" 54 | else 55 | @output.puts " create #{dir}/" 56 | Dir.mkdir(dir) 57 | end 58 | end 59 | 60 | def write_file(file, contents) 61 | @output.puts " create #{file}" 62 | File.open(file, 'w') {|file| file.puts contents } 63 | end 64 | 65 | def test 66 | < 'mruby-print' 106 | spec.add_dependency 'mruby-mtest', :mgem => 'mruby-mtest' 107 | end 108 | 109 | spec.license = 'MIT' 110 | spec.author = 'MRuby Developer' 111 | spec.summary = '#{@name}' 112 | spec.version = #{Util.camelize(@name)}::VERSION 113 | MRBGEM_RAKE 114 | end 115 | 116 | def gitignore 117 | < 235 | #include 236 | 237 | /* Include the mruby header */ 238 | #include 239 | #include 240 | 241 | int main(int argc, char *argv[]) 242 | { 243 | mrb_state *mrb = mrb_open(); 244 | mrb_value ARGV = mrb_ary_new_capa(mrb, argc); 245 | int i; 246 | int return_value; 247 | 248 | for (i = 0; i < argc; i++) { 249 | mrb_ary_push(mrb, ARGV, mrb_str_new_cstr(mrb, argv[i])); 250 | } 251 | mrb_define_global_const(mrb, "ARGV", ARGV); 252 | 253 | // call __main__(ARGV) 254 | mrb_funcall(mrb, mrb_top_self(mrb), "__main__", 1, ARGV); 255 | 256 | return_value = EXIT_SUCCESS; 257 | 258 | if (mrb->exc) { 259 | mrb_print_error(mrb); 260 | return_value = EXIT_FAILURE; 261 | } 262 | mrb_close(mrb); 263 | 264 | return return_value; 265 | } 266 | TOOLS 267 | end 268 | 269 | def mrblib 270 | < [:all] do 353 | Dir.chdir(mruby_root) do 354 | MRuby.each_target do |target| 355 | `\#{target.cc.command} --version` 356 | abort("Command \#{target.cc.command} for \#{target.name} is missing.") unless $?.success? 357 | end 358 | %W(\#{mruby_root}/build/x86_64-pc-linux-gnu/bin/\#{APP_NAME} \#{mruby_root}/build/i686-pc-linux-gnu/\#{APP_NAME}).each do |bin| 359 | sh "strip --strip-unneeded \#{bin}" if File.exist?(bin) 360 | end 361 | end 362 | end 363 | 364 | namespace :test do 365 | desc "run mruby & unit tests" 366 | # only build mtest for host 367 | task :mtest => :compile do 368 | Dir.chdir(mruby_root) do 369 | # in order to get mruby/test/t/synatx.rb __FILE__ to pass, 370 | # we need to make sure the tests are built relative from mruby_root 371 | MRuby.each_target do |target| 372 | # only run unit tests here 373 | target.enable_bintest = false 374 | run_test if target.test_enabled? 375 | end 376 | end 377 | end 378 | 379 | def clean_env(envs) 380 | old_env = {} 381 | envs.each do |key| 382 | old_env[key] = ENV[key] 383 | ENV[key] = nil 384 | end 385 | yield 386 | envs.each do |key| 387 | ENV[key] = old_env[key] 388 | end 389 | end 390 | 391 | desc "run integration tests" 392 | task :bintest => :compile do 393 | Dir.chdir(mruby_root) do 394 | MRuby.each_target do |target| 395 | clean_env(%w(MRUBY_ROOT MRUBY_CONFIG)) do 396 | run_bintest if target.bintest_enabled? 397 | end 398 | end 399 | end 400 | end 401 | end 402 | 403 | desc "run all tests" 404 | Rake::Task['test'].clear 405 | task :test => ["test:mtest", "test:bintest"] 406 | 407 | desc "cleanup" 408 | task :clean do 409 | Dir.chdir(mruby_root) do 410 | sh "rake deep_clean" 411 | end 412 | end 413 | 414 | desc "generate a release tarball" 415 | task :release => :compile do 416 | require 'tmpdir' 417 | 418 | Dir.chdir(mruby_root) do 419 | # since we're in the mruby/ 420 | release_dir = "releases/v\#{APP_VERSION}" 421 | release_path = Dir.pwd + "/../\#{release_dir}" 422 | app_name = "\#{APP_NAME}-\#{APP_VERSION}" 423 | FileUtils.mkdir_p(release_path) 424 | 425 | Dir.mktmpdir do |tmp_dir| 426 | Dir.chdir(tmp_dir) do 427 | MRuby.each_target do |target| 428 | next if name == "host" 429 | 430 | arch = name 431 | bin = "\#{build_dir}/bin/\#{exefile(APP_NAME)}" 432 | FileUtils.mkdir_p(name) 433 | FileUtils.cp(bin, name) 434 | 435 | Dir.chdir(arch) do 436 | arch_release = "\#{app_name}-\#{arch}" 437 | puts "Writing \#{release_dir}/\#{arch_release}.tgz" 438 | `tar czf \#{release_path}/\#{arch_release}.tgz *` 439 | end 440 | end 441 | 442 | puts "Writing \#{release_dir}/\#{app_name}.tgz" 443 | `tar czf \#{release_path}/\#{app_name}.tgz *` 444 | end 445 | end 446 | end 447 | end 448 | 449 | namespace :local do 450 | desc "show version" 451 | task :version do 452 | puts "\#{APP_NAME} \#{APP_VERSION}" 453 | end 454 | end 455 | 456 | def is_in_a_docker_container? 457 | `grep -q docker /proc/self/cgroup` 458 | $?.success? 459 | end 460 | 461 | Rake.application.tasks.each do |task| 462 | next if ENV["MRUBY_CLI_LOCAL"] 463 | unless task.name.start_with?("local:") 464 | # Inspired by rake-hooks 465 | # https://github.com/guillermo/rake-hooks 466 | old_task = Rake.application.instance_variable_get('@tasks').delete(task.name) 467 | desc old_task.full_comment 468 | task old_task.name => old_task.prerequisites do 469 | abort("Not running in docker, you should type \\"docker-compose run \\".") \ 470 | unless is_in_a_docker_container? 471 | old_task.invoke 472 | end 473 | end 474 | end 475 | RAKEFILE 476 | end 477 | end 478 | end 479 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/util.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class Util 3 | class << self 4 | def camelize(string) 5 | string.split("-").map {|w| w.capitalize }.map {|w| 6 | w.split("_").map {|w2| w2.capitalize }.join('') 7 | }.join('') 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /mrblib/mruby-cli/version.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class Version 3 | VERSION = "0.0.4" 4 | 5 | def initialize(output_io) 6 | @output_io = output_io 7 | end 8 | 9 | def run 10 | @output_io.puts "mruby-cli version #{VERSION}" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/mruby-cli/test_option.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class TestOption < MTest::Unit::TestCase 3 | def test_to_long_opt_true 4 | option = Option.new("setup", "s", true) 5 | 6 | assert_equal "setup:", option.to_long_opt 7 | end 8 | 9 | def test_to_short_opt_true 10 | option = Option.new("setup", "s", true) 11 | 12 | assert_equal "s:", option.to_short_opt 13 | end 14 | 15 | def test_to_long_opt_false 16 | option = Option.new("version", "v") 17 | 18 | assert_equal "version", option.to_long_opt 19 | end 20 | 21 | def test_to_short_opt_false 22 | option = Option.new("version", "v") 23 | 24 | assert_equal "v", option.to_short_opt 25 | end 26 | end 27 | end 28 | 29 | MTest::Unit.new.run 30 | -------------------------------------------------------------------------------- /test/mruby-cli/test_options.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class TestOptions < MTest::Unit::TestCase 3 | def test_add 4 | options = Options.new 5 | options.add(Option.new("setup", "s", true)) 6 | options.add(Option.new("version", "v", false)) 7 | 8 | assert_equal %w(setup: version), options.long_opts 9 | assert_equal "s:v", options.short_opts 10 | end 11 | 12 | def test_option 13 | options = Options.new 14 | options.add(Option.new("setup", "s", true)) 15 | options.add(Option.new("version", "v", false)) 16 | 17 | options.parsed_opts = {"setup" => "foo"} 18 | assert_equal "foo", options.option(:setup) 19 | 20 | options.parsed_opts = {"s" => "foo"} 21 | assert_equal "foo", options.option(:setup) 22 | 23 | options.parsed_opts = {"version" => ""} 24 | assert_equal "", options.option(:version) 25 | 26 | options.parsed_opts = {"v" => ""} 27 | assert_equal "", options.option(:version) 28 | 29 | options.parsed_opts = {"v" => ""} 30 | assert_equal false, options.option(:setup) 31 | 32 | options.parsed_opts = {"v" => ""} 33 | assert_equal nil, options.option(:blah) 34 | end 35 | end 36 | end 37 | 38 | MTest::Unit.new.run 39 | -------------------------------------------------------------------------------- /test/mruby-cli/test_util.rb: -------------------------------------------------------------------------------- 1 | module MRubyCLI 2 | class TestUtil < MTest::Unit::TestCase 3 | def test_camelize 4 | assert_equal "GoodNightMoon", Util.camelize("good_night_moon") 5 | assert_equal "FooBarBaz", Util.camelize("foo-bar-baz") 6 | end 7 | end 8 | end 9 | 10 | MTest::Unit.new.run 11 | -------------------------------------------------------------------------------- /tools/mruby-cli/mruby-cli.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* Include the mruby header */ 5 | #include 6 | #include 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | mrb_state *mrb = mrb_open(); 11 | mrb_value ARGV = mrb_ary_new_capa(mrb, argc); 12 | int i; 13 | int return_value; 14 | 15 | for (i = 0; i < argc; i++) { 16 | mrb_ary_push(mrb, ARGV, mrb_str_new_cstr(mrb, argv[i])); 17 | } 18 | mrb_define_global_const(mrb, "ARGV", ARGV); 19 | 20 | // call __main__(ARGV) 21 | mrb_funcall(mrb, mrb_top_self(mrb), "__main__", 1, ARGV); 22 | 23 | return_value = EXIT_SUCCESS; 24 | 25 | if (mrb->exc) { 26 | mrb_print_error(mrb); 27 | return_value = EXIT_FAILURE; 28 | } 29 | mrb_close(mrb); 30 | 31 | return return_value; 32 | } 33 | --------------------------------------------------------------------------------