├── .gitignore ├── .overcommit.yml ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── exe └── mwc ├── lib ├── mwc.rb └── mwc │ ├── command.rb │ ├── commands │ ├── compile.rb │ ├── init.rb │ ├── server.rb │ └── watch.rb │ ├── compile_options.rb │ ├── config.rb │ ├── environment.rb │ ├── options │ ├── mruby.rb │ └── project.rb │ ├── server.rb │ ├── tasks.rb │ ├── templates │ ├── app │ │ ├── .gitignore │ │ ├── config │ │ │ └── build.rb │ │ └── src │ │ │ └── main.c │ └── mwcrc.erb │ ├── utils │ ├── command.rb │ ├── command_registry.rb │ ├── hash_accessor.rb │ └── option.rb │ └── version.rb ├── mwc.gemspec └── spec ├── masm_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Use this file to configure the Overcommit hooks you wish to use. This will 2 | # extend the default configuration defined in: 3 | # https://github.com/sds/overcommit/blob/master/config/default.yml 4 | # 5 | # At the topmost level of this YAML file is a key representing type of hook 6 | # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can 7 | # customize each hook, such as whether to only run it on certain files (via 8 | # `include`), whether to only display output if it fails (via `quiet`), etc. 9 | # 10 | # For a complete list of hooks, see: 11 | # https://github.com/sds/overcommit/tree/master/lib/overcommit/hook 12 | # 13 | # For a complete list of options that you can use to customize hooks, see: 14 | # https://github.com/sds/overcommit#configuration 15 | # 16 | # Uncomment the following lines to make the configuration take effect. 17 | 18 | PreCommit: 19 | AuthorName: 20 | enabled: false 21 | 22 | RuboCop: 23 | enabled: true 24 | on_warn: fail # Treat all warnings as failures 25 | 26 | TrailingWhitespace: 27 | enabled: true 28 | 29 | PostCheckout: 30 | ALL: # Special hook name that customizes all hooks of this type 31 | quiet: true # Change all post-checkout hooks to only display output on failure 32 | 33 | IndexTags: 34 | enabled: true # Generate a tags file with `ctags` each time HEAD changes 35 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.6.3 7 | before_install: gem install bundler -v 2.0.2 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at elct9620@frost.tw. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in mwc.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mwc (0.4.0) 5 | listen (~> 3.2.0) 6 | rack (>= 2.0.7, < 3.1.0) 7 | rake (>= 10, < 14) 8 | thor (~> 0.20.3) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | ast (2.4.0) 14 | bundler-audit (0.6.1) 15 | bundler (>= 1.2.0, < 3) 16 | thor (~> 0.18) 17 | childprocess (3.0.0) 18 | diff-lcs (1.3) 19 | ffi (1.13.1) 20 | iniparse (1.4.4) 21 | jaro_winkler (1.5.4) 22 | listen (3.2.1) 23 | rb-fsevent (~> 0.10, >= 0.10.3) 24 | rb-inotify (~> 0.9, >= 0.9.10) 25 | overcommit (0.51.0) 26 | childprocess (>= 0.6.3, < 4) 27 | iniparse (~> 1.4) 28 | parallel (1.19.1) 29 | parser (2.6.5.0) 30 | ast (~> 2.4.0) 31 | rack (3.0.6.1) 32 | rainbow (3.0.0) 33 | rake (13.0.1) 34 | rb-fsevent (0.10.4) 35 | rb-inotify (0.10.1) 36 | ffi (~> 1.0) 37 | rspec (3.9.0) 38 | rspec-core (~> 3.9.0) 39 | rspec-expectations (~> 3.9.0) 40 | rspec-mocks (~> 3.9.0) 41 | rspec-core (3.9.0) 42 | rspec-support (~> 3.9.0) 43 | rspec-expectations (3.9.0) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.9.0) 46 | rspec-mocks (3.9.0) 47 | diff-lcs (>= 1.2.0, < 2.0) 48 | rspec-support (~> 3.9.0) 49 | rspec-support (3.9.0) 50 | rubocop (0.76.0) 51 | jaro_winkler (~> 1.5.1) 52 | parallel (~> 1.10) 53 | parser (>= 2.6) 54 | rainbow (>= 2.2.2, < 4.0) 55 | ruby-progressbar (~> 1.7) 56 | unicode-display_width (>= 1.4.0, < 1.7) 57 | ruby-progressbar (1.10.1) 58 | thor (0.20.3) 59 | unicode-display_width (1.6.0) 60 | 61 | PLATFORMS 62 | ruby 63 | 64 | DEPENDENCIES 65 | bundler (~> 2.0) 66 | bundler-audit (~> 0.6.1) 67 | mwc! 68 | overcommit (~> 0.51.0) 69 | rspec (~> 3.0) 70 | rubocop (~> 0.76.0) 71 | 72 | BUNDLED WITH 73 | 2.0.2 74 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 蒼時弦也 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 | MWC 2 | === 3 | [![Gem Version](https://badge.fury.io/rb/mwc.svg)](https://badge.fury.io/rb/mwc) [![Build Status](https://travis-ci.com/elct9620/mwc.svg?branch=master)](https://travis-ci.com/elct9620/mwc) [![Maintainability](https://api.codeclimate.com/v1/badges/ecd47b22321830b73d78/maintainability)](https://codeclimate.com/github/elct9620/mwc/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/ecd47b22321830b73d78/test_coverage)](https://codeclimate.com/github/elct9620/mwc/test_coverage) 4 | 5 | The tool for the developer to help them create mruby applications on the WebAssembly. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'mwc' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install mwc 22 | 23 | ## Requirement 24 | 25 | * Curl 26 | * Tar 27 | * Emscripten SDK 28 | * Ruby 2.6+ 29 | 30 | > Please make sure you can execute `emcc` before use `mwc compile` 31 | 32 | ## Usage 33 | 34 | ### Create Project 35 | 36 | Execute below command with your project name: 37 | 38 | $ mwc init my_mrb 39 | 40 | This gem will create a directory `my_mrb` with anything you need to play with mruby on WebAssembly. 41 | 42 | ### Source code detect 43 | 44 | * `src/**/*.c` the normal C code 45 | * `src/js/**/*.lib.js` the JavaScript library can be called in C 46 | * `src/js/**/*.pre.js` the JavaScript prepend to WebAssembly JS 47 | * `src/js/**/*.post.js` the JavaScript append to WebAssembly JS 48 | 49 | ### Compile 50 | 51 | To compile `*.c` to `.wasm` you have to execute `compile` command: 52 | 53 | $ mwc compile 54 | 55 | You can specify compile environment to change with different options: 56 | 57 | $ mwc compile --env=dev 58 | 59 | To see more usage with `help` command: 60 | 61 | $ mwc help compile 62 | 63 | ### Serve compiled files 64 | 65 | The `mwc` has built-in static file server to help preview or debug: 66 | 67 | $ mwc server 68 | 69 | And then, open the `http://localhost:8080` you will see the Emscripten web shell and `Hello World` is printed. 70 | 71 | ## Configure 72 | 73 | We use DSL to define the compile preferences in `.mwcrc` 74 | 75 | ```ruby 76 | project.name = 'mruby' 77 | mruby.version = '2.1.3' 78 | 79 | env :dev do 80 | project.source_map = true 81 | end 82 | ``` 83 | 84 | ### Project 85 | 86 | |Name|Type|Description 87 | |----|----|----------- 88 | |name|string| The project name, will change the generated file name. ex. `mruby.wasm` 89 | |shell|string| The shell file template, if you want to use your own html template 90 | |source_map|boolean| Enable source map for debug 91 | |options|array| Extra compile options. ex. `-s ALLOW_MEMORY_GROWTH=1` 92 | 93 | ### mruby 94 | 95 | |Name|Type|Description 96 | |----|----|----------- 97 | |version|string| The prefer mruby version 98 | 99 | ## Roadmap 100 | 101 | * [ ] Unit Test 102 | * [ ] Download options for mruby 103 | * [x] Archive 104 | * [ ] Git Submodule 105 | * [ ] Integrate to Webpack 106 | * [ ] Watch Mode 107 | * [x] Auto re-compile 108 | * [ ] Add LiveReload support 109 | 110 | ## Development 111 | 112 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 113 | 114 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 115 | 116 | ## Contributing 117 | 118 | Bug reports and pull requests are welcome on GitHub at https://github.com/elct9620/mwc. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 119 | 120 | ## License 121 | 122 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 123 | 124 | ## Code of Conduct 125 | 126 | Everyone interacting in the Mwc project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/elct9620/mwc/blob/master/CODE_OF_CONDUCT.md). 127 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'mwc' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /exe/mwc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'mwc/command' 5 | 6 | Mwc::Command.start(ARGV) 7 | -------------------------------------------------------------------------------- /lib/mwc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | 5 | require 'mwc/version' 6 | require 'mwc/config' 7 | 8 | # WebAssembly compile tool for mruby 9 | module Mwc 10 | # @since 0.3.0 11 | # @api private 12 | LOCK = Mutex.new 13 | 14 | # The project root 15 | # 16 | # @return [Pathname] the root 17 | # 18 | # @since 0.1.0 19 | # @api private 20 | def self.root 21 | return @root unless @root.nil? 22 | 23 | @root ||= Pathname.pwd 24 | @root ||= Bundler.root if defined?(::Bundler) 25 | @root 26 | end 27 | 28 | # Set project root 29 | # 30 | # @param path [String|Pathname] the root path 31 | # 32 | # @since 0.1.0 33 | # @api private 34 | def self.root=(path) 35 | @root = Pathname.new(path) 36 | end 37 | 38 | # The mwc config 39 | # 40 | # @return [Mwc::Config] the config 41 | # 42 | # @since 0.1.0 43 | # @api private 44 | def self.config 45 | @config ||= Config.new 46 | end 47 | 48 | # Set config 49 | # 50 | # @param path [Pathname] the config path 51 | # 52 | # @since 0.3.0 53 | # @api private 54 | def self.config=(path) 55 | @config = Config.new(path) 56 | end 57 | 58 | # The thor template source root 59 | # 60 | # @return [String] the source root path 61 | # 62 | # @since 0.1.0 63 | # @api private 64 | def self.source_root 65 | Pathname 66 | .new(File.dirname(__FILE__)) 67 | .join('mwc', 'templates') 68 | .to_s 69 | end 70 | 71 | # Use prefer environment 72 | # 73 | # @param name [String] prefer environment 74 | # @param block [Proc] the block execute under this environment 75 | # 76 | # @since 0.3.0 77 | # @api private 78 | def self.use(env, &_block) 79 | LOCK.synchronize do 80 | @env = env&.to_sym 81 | yield 82 | @env = nil 83 | end 84 | end 85 | 86 | # Current environment 87 | # 88 | # @see Mwc::Environment 89 | # 90 | # @return [Mwc::Environment] the environment 91 | # 92 | # @since 0.3.0 93 | # @api private 94 | def self.environment 95 | return config.default if @env.nil? 96 | 97 | config.environments[@env] || config.default 98 | end 99 | 100 | # Current mruby preferences 101 | # 102 | # @return [Mwc::Options::MRuby] the mruby options 103 | # 104 | # @since 0.3.0 105 | # @api private 106 | def self.mruby 107 | environment.mruby 108 | end 109 | 110 | # Current project preferences 111 | # 112 | # @return [Mwc::Options::Project] the project options 113 | # 114 | # @since 0.3.0 115 | # @api private 116 | def self.project 117 | environment.project 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/mwc/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor' 4 | 5 | require 'mwc/utils/command_registry' 6 | require 'mwc/config' 7 | require 'mwc/commands/init' 8 | require 'mwc/commands/compile' 9 | require 'mwc/commands/watch' 10 | require 'mwc/commands/server' 11 | 12 | module Mwc 13 | # :nodoc: 14 | class Command < Thor 15 | include Utils::CommandRegistry 16 | 17 | class_option :env, desc: 'the prefer environment' 18 | 19 | desc 'version', 'show version' 20 | def version 21 | puts Mwc::VERSION 22 | end 23 | 24 | add_command Commands::Init 25 | add_command Commands::Compile 26 | add_command Commands::Watch 27 | add_command Commands::Server 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/mwc/commands/compile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor/rake_compat' 4 | 5 | require 'mwc/utils/command' 6 | require 'mwc/tasks' 7 | require 'mwc' 8 | 9 | module Mwc 10 | module Commands 11 | # Compile mruby to wasm 12 | class Compile < Thor::Group 13 | include Thor::Actions 14 | include Utils::Command 15 | 16 | name 'compile' 17 | description 'compile source code to wasm' 18 | display_on { Mwc.config.exist? } 19 | add_option :format, default: 'html', enum: %w[html js wasm] 20 | 21 | def compile 22 | Mwc.use(parent_options['env']) do 23 | # TODO: Allow change output directory 24 | empty_directory('dist') 25 | 26 | Tasks.new 27 | Rake::Task[parent_options['format']].invoke 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mwc/commands/init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc/utils/command' 4 | require 'mwc' 5 | 6 | module Mwc 7 | module Commands 8 | # Create a new project 9 | class Init < Thor::Group 10 | include Thor::Actions 11 | include Utils::Command 12 | 13 | name 'init' 14 | usage 'init NAME' 15 | description 'create a new project' 16 | display_on { !Mwc.config.exist? } 17 | argument :name, type: :string, desc: 'project name' 18 | 19 | def create_project 20 | directory('app', name) 21 | self.destination_root = name 22 | Mwc.root = destination_root 23 | end 24 | 25 | # :nodoc: 26 | def create_mwcrc 27 | template('mwcrc.erb', '.mwcrc') 28 | Mwc.config = Pathname.new(destination_root).join('.mwcrc') 29 | end 30 | 31 | # :nodoc: 32 | def download_mruby 33 | # TODO: Allow choose download mode 34 | empty_directory('vendor') 35 | inside(mruby_directory.dirname) do 36 | run("curl -OL #{archive_url}") 37 | run("tar -zxf #{filename}") 38 | remove_file(filename) 39 | run("mv mruby-#{version} #{mruby_directory}") 40 | end 41 | end 42 | 43 | private 44 | 45 | # :nodoc: 46 | def version 47 | Mwc.mruby.version 48 | end 49 | 50 | # :nodoc: 51 | def archive_url 52 | "https://github.com/mruby/mruby/archive/#{filename}" 53 | end 54 | 55 | # :nodoc: 56 | def filename 57 | "#{version}.tar.gz" 58 | end 59 | 60 | # :nodoc: 61 | def mruby_directory 62 | Mwc.mruby.path 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/mwc/commands/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack' 4 | 5 | require 'mwc/utils/command' 6 | require 'mwc/server' 7 | require 'mwc' 8 | 9 | module Mwc 10 | module Commands 11 | # :nodoc: 12 | class Server < Thor::Group 13 | include Utils::Command 14 | 15 | name 'server' 16 | description 'serve compiled wasm' 17 | add_option :port, type: :numeric, default: 8080 18 | 19 | def boot 20 | Rack::Handler 21 | .default 22 | .run(Mwc::Server.new, Port: parent_options['port']) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mwc/commands/watch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'listen' 4 | 5 | require 'mwc/utils/command' 6 | require 'mwc/tasks' 7 | require 'mwc' 8 | 9 | module Mwc 10 | module Commands 11 | # :nodoc: 12 | class Watch < Thor::Group 13 | include Thor::Actions 14 | include Utils::Command 15 | 16 | name 'watch' 17 | description 'watch src changes and auto re-compile' 18 | add_option :format, default: 'html', enum: %w[html js wasm] 19 | 20 | DESIRE_FILES = /\.(h|c|cpp|js)$/.freeze 21 | 22 | def prepare 23 | @stopped = false 24 | @dirs = [ 25 | Mwc.root.join('src'), 26 | Mwc.root.join('include') 27 | ].map(&:to_s) 28 | end 29 | 30 | def setup_tasks 31 | Tasks.new 32 | end 33 | 34 | def setup_listener 35 | @listener = Listen.to(*@dirs, only: DESIRE_FILES) do |*_| 36 | Mwc.use(parent_options['env']) do 37 | # TODO: Allow change output directory 38 | empty_directory('dist') 39 | 40 | compile 41 | end 42 | end 43 | end 44 | 45 | def start 46 | puts 'Starting watch file changes...' 47 | @listener.start 48 | 49 | Signal.trap(:INT) { exit } 50 | sleep 51 | end 52 | 53 | private 54 | 55 | def task 56 | Rake::Task[parent_options['format']] 57 | end 58 | 59 | def compile 60 | task.invoke 61 | puts 'Compiled!' 62 | rescue RuntimeError 63 | puts 'Compile Failed' 64 | ensure 65 | reset 66 | end 67 | 68 | def reset 69 | task.all_prerequisite_tasks.each(&:reenable) 70 | task.reenable 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/mwc/compile_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc' 4 | 5 | module Mwc 6 | # The compile options 7 | class CompileOptions 8 | # :nodoc: 9 | EXTRA_JS_TYPE = { 10 | library_js: '--js-library', 11 | pre_js: '--pre-js', 12 | post_js: '--post-js' 13 | }.freeze 14 | 15 | OPTIONS = %i[shell source_map extra].freeze 16 | 17 | # :nodoc: 18 | def initialize(options = {}) 19 | @options = [] 20 | 21 | options.each do |name, value| 22 | handler = "add_#{name}" 23 | send(handler, value) if respond_to?(handler) 24 | end 25 | 26 | OPTIONS.each { |name| send("setup_#{name}") } 27 | output(options[:format]) 28 | end 29 | 30 | # Setup shell file 31 | # 32 | # @since 0.2.0 33 | # @api private 34 | def setup_shell 35 | return if Mwc.project.shell.nil? 36 | 37 | @options.push "--shell-file #{Mwc.project.shell}" 38 | end 39 | 40 | # Setup source map 41 | # 42 | # @since 0.2.0 43 | # @api private 44 | def setup_source_map 45 | return unless Mwc.project.source_map 46 | 47 | @options.push '-g4 --source-map-base /' 48 | end 49 | 50 | # Setup extra options 51 | # 52 | # @since 0.2.0 53 | # @api private 54 | def setup_extra 55 | return unless Mwc.project.options.any? 56 | 57 | Mwc.project.options.each do |option| 58 | @options.push option 59 | end 60 | end 61 | 62 | # Convert options to string 63 | # 64 | # @return [String] the options 65 | def to_s 66 | @options.join(' ') 67 | end 68 | 69 | # Configure extra javacript 70 | # 71 | # @since 0.2.0 72 | # @api private 73 | %i[library_js pre_js post_js].each do |type| 74 | define_method "add_#{type}" do |items| 75 | items.each do |path| 76 | @options.push "#{EXTRA_JS_TYPE[type]} #{path}" 77 | end 78 | end 79 | end 80 | 81 | private 82 | 83 | # Configure output format 84 | # 85 | # @param foramt [String] output format 86 | # 87 | # @since 0.1.0 88 | # @api private 89 | def output(format) 90 | @options.push "-o dist/#{Mwc.project.name}.#{format}" 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/mwc/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | 5 | require 'mwc/environment' 6 | require 'mwc' 7 | 8 | module Mwc 9 | # The compile preferences 10 | class Config 11 | extend Forwardable 12 | 13 | # @since 0.3.0 14 | # @api private 15 | delegate %i[environments] => :@default 16 | 17 | # @since 0.3.0 18 | # @api private 19 | LOCK = Mutex.new 20 | 21 | # @since 0.3.0 22 | # @api private 23 | attr_reader :default 24 | 25 | # :nodoc: 26 | def initialize(path = Mwc.root.join('.mwcrc')) 27 | @path = Pathname.new(path) 28 | @default = Environment.new 29 | load_config if exist? 30 | end 31 | 32 | # Check config file exists 33 | # 34 | # @return [TrueClass,FalseClass] exist or not 35 | # 36 | # @since 0.1.0 37 | # @api private 38 | def exist? 39 | @path.exist? 40 | end 41 | 42 | # Reload config 43 | # 44 | # @since 0.1.0 45 | # @api private 46 | def reload 47 | Mwc.config = Mwc.root.join('.mwcrc') 48 | end 49 | 50 | private 51 | 52 | # Laod .mwcrc config 53 | # 54 | # @since 0.1.0 55 | # @api private 56 | def load_config 57 | LOCK.synchronize { @default.instance_eval(@path.read) } 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/mwc/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc/options/project' 4 | require 'mwc/options/mruby' 5 | 6 | module Mwc 7 | # The compile environment manager 8 | # 9 | # @since 0.3.0 10 | # @api private 11 | class Environment 12 | # @since 0.3.0 13 | # @api private 14 | attr_reader :environments, :project, :mruby 15 | 16 | # @since 0.3.0 17 | # @api private 18 | def initialize(parent = nil, &block) 19 | @parent = parent 20 | @environments = {} 21 | @project = Options::Project.new(parent&.project) 22 | @mruby = Options::MRuby.new(parent&.mruby) 23 | instance_exec(self, &block) if block_given? 24 | end 25 | 26 | # Define new environment 27 | # 28 | # @param name [Symbol] the environment name 29 | # @param block [Proc] the environment config block 30 | # 31 | # @since 0.3.0 32 | # @api private 33 | def env(name, &block) 34 | return if @parent 35 | 36 | @environments[name.to_sym] = Environment.new(self, &block) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mwc/options/mruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc/utils/option' 4 | 5 | module Mwc 6 | module Options 7 | # The mruby preference 8 | class MRuby 9 | include Utils::Option 10 | 11 | option :version 12 | option :path, type: :path, default: 'vendor/mruby' 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mwc/options/project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc/utils/option' 4 | 5 | module Mwc 6 | module Options 7 | # The project related options 8 | class Project 9 | include Utils::Option 10 | 11 | option :name 12 | option :source_map, type: :bool 13 | option :shell, type: :path 14 | option :options, array: true 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mwc/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | require 'forwardable' 5 | require 'rack' 6 | 7 | require 'mwc' 8 | 9 | module Mwc 10 | # Static assets server 11 | class Server 12 | WASM_RULE = /\.(?:wasm)\z/.freeze 13 | WASM_HEADER = { 'Content-Type' => 'application/wasm' }.freeze 14 | 15 | def initialize 16 | @static = 17 | Rack::Static.new( 18 | ->(_) { [404, {}, []] }, 19 | root: 'dist', # TODO: Set by config 20 | index: "#{Mwc.project.name}.html", 21 | urls: [''], 22 | header_rules: [ 23 | [WASM_RULE, WASM_HEADER] 24 | ] 25 | ) 26 | end 27 | 28 | def call(env) 29 | @static.call(env) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mwc/tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake/tasklib' 4 | 5 | require 'mwc/compile_options' 6 | require 'mwc' 7 | 8 | module Mwc 9 | # :nodoc: 10 | class Tasks < Rake::TaskLib 11 | SOURCES = FileList['src/**/*.c'] 12 | BINARIES = SOURCES.ext('bc') 13 | LIBRARY_JS = FileList['src/js/**/*.lib.js'] 14 | PRE_JS = FileList['src/js/**/*.pre.js'] 15 | POST_JS = FileList['src/js/**/*.post.js'] 16 | 17 | # :nodoc: 18 | def initialize 19 | return unless mruby_directory.join('Rakefile').exist? 20 | 21 | namespace :mruby do 22 | ENV['MRUBY_CONFIG'] = Mwc.root.join('config', 'build.rb').to_s 23 | # TODO: Prevent load error breaks command 24 | load mruby_directory.join('Rakefile') 25 | end 26 | 27 | compile_binary_task 28 | compile_wasm_task 29 | end 30 | 31 | private 32 | 33 | # :nodoc: 34 | def compile_binary_task 35 | rule '.bc' => SOURCES do |task| 36 | sh "emcc -I #{mruby_directory.join('include')} -I include -c " \ 37 | "#{task.source} -o #{task.name}" 38 | end 39 | end 40 | 41 | # :nodoc: 42 | def compile_wasm_task 43 | %i[wasm html js].each do |format| 44 | desc "Compile sources to #{format} WebAssembly" 45 | task format => BINARIES do 46 | Rake::Task['mruby:all'].invoke 47 | compile(format) 48 | end 49 | end 50 | end 51 | 52 | # :nodoc: 53 | def compile(format) 54 | do_compile CompileOptions.new( 55 | format: format, 56 | library_js: LIBRARY_JS, 57 | pre_js: PRE_JS, 58 | post_js: POST_JS 59 | ) 60 | end 61 | 62 | # :nodoc: 63 | def do_compile(options) 64 | sh "emcc #{sources.join(' ')} #{options}" 65 | end 66 | 67 | # :nodoc: 68 | def libmruby 69 | mruby_directory.join('build/wasm/lib/libmruby.bc') 70 | end 71 | 72 | # :nodoc: 73 | def sources 74 | [libmruby].concat(BINARIES) 75 | end 76 | 77 | # :nodoc: 78 | def mruby_directory 79 | Mwc.mruby.path 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/mwc/templates/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Useless system files 2 | .DS_Store 3 | 4 | # Vendor 5 | vendor/mruby 6 | 7 | # Compiled 8 | *.wasm 9 | *.bc 10 | *.map 11 | dist/ 12 | -------------------------------------------------------------------------------- /lib/mwc/templates/app/config/build.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | MRuby::Build.new do |conf| 4 | if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR'] 5 | toolchain :visualcpp 6 | else 7 | toolchain :gcc 8 | end 9 | 10 | conf.gembox 'default' 11 | end 12 | 13 | MRuby::CrossBuild.new('wasm') do |conf| 14 | toolchain :clang 15 | 16 | # C compiler settings 17 | conf.cc do |cc| 18 | cc.command = 'emcc' 19 | cc.compile_options = '%s -s WASM=1 -o %s ' \ 20 | '-c %s -Oz --llvm-opts 3' 21 | end 22 | 23 | # Linker settings 24 | conf.linker do |linker| 25 | linker.command = 'emcc' 26 | linker.link_options = '%s -o %s %s %s' 27 | end 28 | 29 | # Archiver settings 30 | conf.archiver do |archiver| 31 | archiver.command = 'emcc' 32 | archiver.archive_options = '%s -s WASM=1 -o %s' 33 | end 34 | 35 | # file extensions 36 | conf.exts do |exts| 37 | exts.object = '.bc' 38 | exts.executable = '' # '.exe' if Windows 39 | exts.library = '.bc' 40 | end 41 | 42 | # TODO: Allow specify customize gembox 43 | conf.gembox 'default' 44 | end 45 | -------------------------------------------------------------------------------- /lib/mwc/templates/app/src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | mrb_state* mrb = mrb_open(); 7 | mrb_load_string(mrb, "puts 'Hello, mruby on WebAssembly'"); 8 | mrb_close(mrb); 9 | 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /lib/mwc/templates/mwcrc.erb: -------------------------------------------------------------------------------- 1 | # vi: set ft=ruby : 2 | 3 | # frozen_string_literal: true 4 | 5 | # Project settings 6 | project.name = '<%= name %>' 7 | # project.shell = 'src/shell.html' 8 | # project.options '-s ALLOW_MEMORY_GROWTH=1' 9 | 10 | # mruby settings 11 | mruby.version = '2.1.0' 12 | 13 | env :dev do 14 | project.source_map = true 15 | end 16 | -------------------------------------------------------------------------------- /lib/mwc/utils/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc' 4 | 5 | module Mwc 6 | module Utils 7 | # The command extensions 8 | module Command 9 | # :nodoc: 10 | def self.included(base) 11 | base.extend ClassMethods 12 | end 13 | 14 | # :nodoc: 15 | module ClassMethods 16 | # Get or set command name 17 | # 18 | # @param name [String] the command name 19 | # 20 | # @since 0.1.0 21 | # @api private 22 | def name(name = nil) 23 | return @name || self.name.split('::').last.downcase if name.nil? 24 | 25 | @name = name 26 | end 27 | 28 | # Set command usage 29 | # 30 | # @param name [String] the command usage 31 | # 32 | # @since 0.1.0 33 | # @api private 34 | def usage(usage = nil) 35 | return @usage || name if usage.nil? 36 | 37 | @usage = usage 38 | end 39 | 40 | # Get or set command description 41 | # 42 | # @param desc [String] the command description 43 | # 44 | # @since 0.1.0 45 | # @api private 46 | def description(desc = nil) 47 | return @description || name if desc.nil? 48 | 49 | @description = desc 50 | end 51 | 52 | # The command should display or not 53 | # 54 | # @return [TrueClass,FalseClass] display on command 55 | # 56 | # @since 0.1.0 57 | # @api private 58 | def display? 59 | return true if @display.nil? 60 | 61 | @display.call 62 | end 63 | 64 | # Define the command display policy 65 | # 66 | # @param proc [Proc] the display policy block 67 | # 68 | # @since 0.1.0 69 | # @api private 70 | def display_on(&block) 71 | return unless block_given? 72 | 73 | @display = block 74 | end 75 | 76 | # The thor template source root 77 | # 78 | # @see Mwc.source_root 79 | # 80 | # @return [String] the source root path 81 | # 82 | # @since 0.1.0 83 | # @api private 84 | def source_root 85 | Mwc.source_root 86 | end 87 | 88 | # The command options 89 | # 90 | # @since 0.1.0 91 | # @api private 92 | def options 93 | @options ||= [] 94 | end 95 | 96 | # Add command options 97 | # 98 | # @param name [String|Symbol] the option name 99 | # @param options [Hash] the option options 100 | # 101 | # @since 0.1.0 102 | # @api private 103 | def add_option(name, options = {}) 104 | @options ||= [] 105 | @options.push([name, options]) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/mwc/utils/command_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mwc 4 | module Utils 5 | # The helper to register command on thor 6 | module CommandRegistry 7 | # :nodoc: 8 | def self.included(base) 9 | base.extend ClassMethods 10 | end 11 | 12 | # :nodoc: 13 | module ClassMethods 14 | # Add command to thor 15 | # 16 | # @param command [Class] the command to add 17 | # 18 | # @since 0.1.0 19 | # @api private 20 | def add_command(command) 21 | return unless command.display? 22 | 23 | command.options.each { |args| method_option(*args) } 24 | register command, 25 | command.name, 26 | command.usage, 27 | command.description 28 | end 29 | 30 | # Add subcommand to thor 31 | # 32 | # @param command [Class] the subcommand 33 | # 34 | # @since 0.1.0 35 | # @api private 36 | def add_subcommand(command) 37 | return unless command.display? 38 | 39 | desc command.usage, command.description 40 | subcommand command.name, command 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mwc/utils/hash_accessor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mwc 4 | module Utils 5 | # Provide Hash-like accessor 6 | module HashAccessor 7 | # Hash-like getter 8 | # 9 | # @param name [String|Symbol] the option name 10 | # 11 | # @since 0.3.0 12 | # @api private 13 | def [](name) 14 | return unless respond_to?(name) 15 | 16 | send(name) 17 | end 18 | 19 | # Hash-like setter 20 | # 21 | # @param name [String|Symbol] the option name 22 | # @param value [Object] the option value 23 | # 24 | # @since 0.3.0 25 | # @api private 26 | def []=(name, value) 27 | return unless respond_to?("#{name}=") 28 | 29 | send("#{name}=", value) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mwc/utils/option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mwc/utils/hash_accessor' 4 | 5 | module Mwc 6 | module Utils 7 | # Extend option class 8 | module Option 9 | # :nodoc: 10 | def self.included(base) 11 | base.class_eval do 12 | extend ClassMethods 13 | include HashAccessor 14 | 15 | def initialize(parent) 16 | @parent = parent 17 | end 18 | end 19 | end 20 | 21 | # :nodoc: 22 | module ClassMethods 23 | # Define new options 24 | # 25 | # @param name [String] the option name 26 | # 27 | # @since 0.3.0 28 | # @api private 29 | def option(name, options = {}) 30 | return create_array_option(name, options) if options[:array] == true 31 | 32 | option_reader(name, options) 33 | option_writer(name, options) 34 | end 35 | 36 | # Cast value to specify type 37 | # 38 | # @param value [Object] the origin value 39 | # @param type [Symbol] the destination type 40 | # 41 | # @since 0.3.0 42 | # @api private 43 | def cast(value, type) 44 | return if value.nil? 45 | 46 | case type 47 | when :path then Mwc.root.join(value) 48 | when :bool then value == true 49 | else value 50 | end 51 | end 52 | 53 | private 54 | 55 | def option_reader(name, options = {}) 56 | define_method name do 57 | instance_variable_get("@#{name}") || 58 | @parent&.send(name) || 59 | self.class.cast(options[:default], options[:type]) 60 | end 61 | end 62 | 63 | def option_writer(name, options = {}) 64 | define_method "#{name}=" do |value| 65 | instance_variable_set( 66 | "@#{name}", 67 | self.class.cast(value, options[:type]) 68 | ) 69 | end 70 | end 71 | 72 | def create_array_option(name, options = {}) 73 | define_method name do |value = nil| 74 | current = instance_variable_get("@#{name}")&.dup || [] 75 | return current.concat(@parent&.send(name) || []).uniq if value.nil? 76 | 77 | current.push self.class.cast(value, options[:type]) 78 | instance_variable_set("@#{name}", current) 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/mwc/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mwc 4 | VERSION = '0.4.0' 5 | end 6 | -------------------------------------------------------------------------------- /mwc.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'mwc/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'mwc' 9 | spec.version = Mwc::VERSION 10 | spec.authors = ['蒼時弦也'] 11 | spec.email = ['contact0@frost.tw'] 12 | 13 | spec.summary = 'The command line tool to compile mruby to WebAssembly' 14 | spec.description = 'The command line tool to compile mruby to WebAssembly' 15 | spec.homepage = 'https://github.com/elct9620/mwc' 16 | spec.license = 'MIT' 17 | 18 | spec.metadata['homepage_uri'] = spec.homepage 19 | spec.metadata['source_code_uri'] = 'https://github.com/elct9620/mwc' 20 | # spec.metadata["changelog_uri"] = "TODO" 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files 24 | # in the RubyGem that have been added into git. 25 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 26 | `git ls-files -z` 27 | .split("\x0") 28 | .reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | end 30 | spec.bindir = 'exe' 31 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ['lib'] 33 | 34 | spec.add_runtime_dependency 'listen', '~> 3.2.0' 35 | spec.add_runtime_dependency 'rack', '>= 2.0.7', '< 3.1.0' 36 | spec.add_runtime_dependency 'rake', '>= 10', '< 14' 37 | spec.add_runtime_dependency 'thor', '~> 0.20.3' 38 | 39 | spec.add_development_dependency 'bundler', '~> 2.0' 40 | spec.add_development_dependency 'bundler-audit', '~> 0.6.1' 41 | spec.add_development_dependency 'overcommit', '~> 0.51.0' 42 | spec.add_development_dependency 'rspec', '~> 3.0' 43 | spec.add_development_dependency 'rubocop', '~> 0.76.0' 44 | end 45 | -------------------------------------------------------------------------------- /spec/masm_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mwc do 4 | it 'has a version number' do 5 | expect(Mwc::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'mwc' 5 | 6 | RSpec.configure do |config| 7 | # Enable flags like --only-failures and --next-failure 8 | config.example_status_persistence_file_path = '.rspec_status' 9 | 10 | # Disable RSpec exposing methods globally on `Module` and `main` 11 | config.disable_monkey_patching! 12 | 13 | config.expect_with :rspec do |c| 14 | c.syntax = :expect 15 | end 16 | end 17 | --------------------------------------------------------------------------------