thermite-rails 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +43 -0
  5. data/CHANGELOG.md +14 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +10 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.md +131 -0
  10. data/Rakefile +15 -0
  11. data/bin/console +15 -0
  12. data/bin/rails +16 -0
  13. data/bin/rake +29 -0
  14. data/bin/rspec +29 -0
  15. data/bin/rubocop +29 -0
  16. data/bin/setup +8 -0
  17. data/lib/thermite/rails.rb +31 -0
  18. data/lib/thermite/rails/cargo_runner.rb +20 -0
  19. data/lib/thermite/rails/check_outdated.rb +19 -0
  20. data/lib/thermite/rails/generators/crate_generator.rb +100 -0
  21. data/lib/thermite/rails/generators/install_generator.rb +14 -0
  22. data/lib/thermite/rails/project.rb +88 -0
  23. data/lib/thermite/rails/railtie.rb +37 -0
  24. data/lib/thermite/rails/root_project.rb +46 -0
  25. data/lib/thermite/rails/tasks/project_rake_task.rb +81 -0
  26. data/lib/thermite/rails/tasks/project_spec_task.rb +33 -0
  27. data/lib/thermite/rails/tasks/project_thermite_build_task.rb +21 -0
  28. data/lib/thermite/rails/tasks/project_thermite_clean_task.rb +21 -0
  29. data/lib/thermite/rails/tasks/project_thermite_test_task.rb +21 -0
  30. data/lib/thermite/rails/tasks/project_thermite_update_task.rb +28 -0
  31. data/lib/thermite/rails/tasks/root_project_rake_task.rb +74 -0
  32. data/lib/thermite/rails/tasks/root_project_spec_task.rb +40 -0
  33. data/lib/thermite/rails/tasks/root_project_thermite_build_task.rb +27 -0
  34. data/lib/thermite/rails/tasks/root_project_thermite_clean_task.rb +27 -0
  35. data/lib/thermite/rails/tasks/root_project_thermite_test_task.rb +27 -0
  36. data/lib/thermite/rails/tasks/root_project_thermite_update_task.rb +27 -0
  37. data/lib/thermite/rails/tasks/rspec.rake +14 -0
  38. data/lib/thermite/rails/tasks/thermite.rake +18 -0
  39. data/lib/thermite/rails/version.rb +7 -0
  40. data/thermite-rails.gemspec +47 -0
  41. metadata +237 -0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150).match?(/This file was generated by Bundler/)
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rubocop', 'rubocop')
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rails/version'
4
+ require_relative 'rails/railtie'
5
+
6
+ module Thermite
7
+ module Rails
8
+ # @param root_path [String]
9
+ # @return [String] Path to the root of the (Rails) project.
10
+ def self.find_root(root_path)
11
+ root = File.expand_path(root_path)
12
+
13
+ dir = root
14
+
15
+ loop do
16
+ return dir if yield(dir)
17
+
18
+ new_dir = File.dirname(dir)
19
+ raise "Unable to find root for #{root}" if new_dir == dir
20
+
21
+ dir = new_dir
22
+ end
23
+ end
24
+
25
+ # @return [Thermite::Rails::RootProject]
26
+ def self.root_project
27
+ require_relative 'rails/root_project'
28
+ @root_project ||= Thermite::Rails::RootProject.new(::Rails.root.to_s)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thermite/cargo'
4
+
5
+ module Thermite
6
+ module Rails
7
+ # Just a wrapper object for thermite's {{Thermite::Cargo}} module.
8
+ class CargoRunner
9
+ include Thermite::Cargo
10
+ include FileUtils
11
+
12
+ attr_reader :config
13
+
14
+ # @param config [Thermite::Config]
15
+ def initialize(config)
16
+ @config = config
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermite
4
+ module Rails
5
+ # Rack middleware for ensuring projects have been built.
6
+ class CheckOutdated
7
+ def initialize(app, project)
8
+ @app = app
9
+ @project = project
10
+ @last_check = 0
11
+ end
12
+
13
+ def call(env)
14
+ @project.ensure_built!
15
+ @app.call(env)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thermite/cargo'
4
+ require 'thermite/config'
5
+ require_relative '../project'
6
+
7
+ module Thermite
8
+ module Rails
9
+ class CrateGenerator < ::Rails::Generators::Base
10
+ class CargoRunner
11
+ include Singleton
12
+ include Thermite::Cargo
13
+ end
14
+
15
+ namespace 'thermite:crate'
16
+ desc 'add new thermite crate'
17
+ argument :name, type: :string
18
+
19
+ def start
20
+ say "Creating crate '#{name}'..."
21
+ end
22
+
23
+ def crates_full_path
24
+ @crates_full_path ||= ::Rails.root.join('crates').to_s
25
+ end
26
+
27
+ def project_full_path
28
+ @project_full_path ||= File.join(crates_full_path, name)
29
+ end
30
+
31
+ def project
32
+ @project ||= Thermite::Rails::Project.new(project_full_path)
33
+ end
34
+
35
+ def ext_full_path
36
+ @ext_full_path ||= File.join(project_full_path, 'ext')
37
+ end
38
+
39
+ def make_crates_dir
40
+ empty_directory(crates_full_path)
41
+ end
42
+
43
+ def run_crate_new_and_bundle_gem
44
+ inside(crates_full_path, verbose: true) do
45
+ run "#{CargoRunner.instance.cargo} new #{name}"
46
+ run "bundle gem #{name}"
47
+ end
48
+ end
49
+
50
+ def update_cargo_toml
51
+ gsub_file(project.cargo_toml_path, "\n[dependencies]") do
52
+ %(publish = false\n\n[lib]\ncrate-type = ["cdylib"]\n\n[dependencies])
53
+ end
54
+ end
55
+
56
+ def add_thermite_to_gemspec
57
+ insert_into_file(project.gemspec_path, after: %( spec.require_paths = ["lib"])) do
58
+ lines = %(\n spec.extensions << 'ext/Rakefile'\n\n)
59
+ lines + %( spec.add_runtime_dependency 'thermite', '~> 0')
60
+ end
61
+ end
62
+
63
+ def add_ext_directory
64
+ empty_directory(ext_full_path)
65
+ end
66
+
67
+ def add_crate_ext_rakefile
68
+ rakefile_path = File.join(ext_full_path, 'Rakefile')
69
+
70
+ create_file(rakefile_path) do
71
+ <<~THERM
72
+ require 'thermite/tasks'
73
+
74
+ project_dir = File.dirname(File.dirname(__FILE__))
75
+ Thermite::Tasks.new(cargo_project_path: project_dir, ruby_project_path: project_dir)
76
+ task default: %w(thermite:build)
77
+ THERM
78
+ end
79
+ end
80
+
81
+ def update_crate_rakefile
82
+ append_to_file(project.rakefile_path) do
83
+ %(\nrequire 'thermite/tasks'\n\nThermite::Tasks.new)
84
+ end
85
+ end
86
+
87
+ def fix_rubocops
88
+ run "bundle exec rubocop -a #{project_full_path}" if File.read('Gemfile').include?('rubocop')
89
+ end
90
+
91
+ def update_rails_gemfile
92
+ gem name, path: "crates/#{name}"
93
+ end
94
+
95
+ def bundle_install
96
+ run 'bundle'
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermite
4
+ module Rails
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ namespace 'thermite:install'
7
+ desc 'install thermite'
8
+
9
+ def configure_environment
10
+ environment 'config.thermite.outdated_error = :page_load', env: :development
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tomlrb'
4
+ require 'thermite/config'
5
+
6
+ module Thermite
7
+ module Rails
8
+ # A "project" is a crate that contains both Rust and Ruby code. Usually one
9
+ # of these will live at [rails root]/crates/[project].
10
+ class Project
11
+ class OutdatedBuildError < StandardError
12
+ # @param name [String] The crate name.
13
+ def initialize(name)
14
+ msg = "\n\nThermite crate '#{name}' is outdated. To resolve this issue, " \
15
+ "run `rake thermite:build:#{name}` and restart your server.\n\n"
16
+
17
+ super(msg)
18
+ end
19
+ end
20
+
21
+ attr_reader :project_path, :config
22
+
23
+ # @param project_path [String]
24
+ def initialize(project_path)
25
+ @project_path = find_project(project_path)
26
+ @config = Thermite::Config.new(ruby_project_path: @project_path, cargo_project_path: @project_path)
27
+ end
28
+
29
+ # @return [String] Path to the project's Cargo.toml file.
30
+ def cargo_toml_path
31
+ File.join(@project_path, 'Cargo.toml')
32
+ end
33
+
34
+ # @return [String] Path to the project's Cargo.toml file.
35
+ def gemspec_path
36
+ File.join(@project_path, "#{crate_name}.gemspec")
37
+ end
38
+
39
+ def rakefile_path
40
+ File.join(@project_path, 'Rakefile')
41
+ end
42
+
43
+ # @return [String] Path to `[project root]/spec/`.
44
+ def spec_path
45
+ File.join(@project_path, 'spec')
46
+ end
47
+
48
+ # @return [String] "package.name" from the crate's Cargo.toml file.
49
+ def crate_name
50
+ @crate_name ||= Tomlrb.load_file(cargo_toml_path)['package']['name']
51
+ end
52
+
53
+ # @return [Boolean] Does this project have `[project root]/spec/`?
54
+ def specs?
55
+ File.exist?(spec_path)
56
+ end
57
+
58
+ # @return [Boolean] Does this project use thermite?
59
+ def thermite?
60
+ return false unless File.exist?(gemspec_path)
61
+
62
+ File.read(gemspec_path).include? 'thermite'
63
+ end
64
+
65
+ # @return [Boolean] Has the crate been updated since it was last built?
66
+ def outdated_build?
67
+ mtime = Dir["#{@project_path}/src/**/*.rs"].map { |file| File.mtime(file) }.max
68
+ native = "#{@project_path}/lib/#{@config.shared_library}"
69
+
70
+ !File.exist?(native) || File.mtime(native) < mtime
71
+ end
72
+
73
+ # Raise if the project is outdated.
74
+ def ensure_built!
75
+ raise OutdatedBuildError, crate_name if outdated_build?
76
+ end
77
+
78
+ private
79
+
80
+ # @param project_path [String]
81
+ def find_project(project_path)
82
+ Thermite::Rails.find_root(project_path) do |dir|
83
+ File.exist?("#{dir}/Cargo.toml")
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+ require_relative 'check_outdated'
5
+ require_relative 'root_project'
6
+
7
+ module Thermite
8
+ module Rails
9
+ class Railtie < ::Rails::Railtie
10
+ config.thermite = ActiveSupport::OrderedOptions.new
11
+
12
+ rake_tasks do
13
+ load File.expand_path(File.join('tasks', 'thermite.rake'), __dir__)
14
+ load File.expand_path(File.join('tasks', 'rspec.rake'), __dir__) if defined?(::RSpec)
15
+ end
16
+
17
+ generators do
18
+ require_relative 'generators/crate_generator'
19
+ require_relative 'generators/install_generator'
20
+ end
21
+
22
+ initializer 'thermite.build_check' do |app|
23
+ project = Thermite::Rails::RootProject.new(app.root)
24
+
25
+ # This will be added by the install generator, but people may forget to run it
26
+ if ::Rails.env.development? && !config.thermite.key?(:outdated_error)
27
+ config.thermite.outdated_error = :page_load
28
+ end
29
+
30
+ if config.thermite.delete(:outdated_error) == :page_load
31
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
32
+ Thermite::Rails::CheckOutdated, project
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'project'
4
+
5
+ module Thermite
6
+ module Rails
7
+ # A root project is really just the Rails project, which can have a number of
8
+ # nested "projects" under crates/[project name].
9
+ class RootProject
10
+ attr_reader :path
11
+
12
+ # @param path [String]
13
+ def initialize(path)
14
+ @path = find_root(path)
15
+ end
16
+
17
+ # @return [Array<Thermite::Rails::Project>]
18
+ def projects
19
+ @projects ||= Dir["#{@path}/crates/*"].
20
+ find_all { |f| File.exist?("#{f}/Cargo.toml") }.
21
+ find_all { |f| File.exist?("#{f}/Cargo.toml") }.
22
+ map { |d| Thermite::Rails::Project.new(d) }.
23
+ find_all(&:thermite?)
24
+ end
25
+
26
+ # @return [Array<Thermite::Rails::Project>]
27
+ def projects_with_specs
28
+ projects.find_all(&:specs?)
29
+ end
30
+
31
+ def ensure_built!
32
+ projects.each(&:ensure_built!)
33
+ end
34
+
35
+ private
36
+
37
+ # @param path [String]
38
+ # @return [String]
39
+ def find_root(path)
40
+ Thermite::Rails.find_root(path) do |dir|
41
+ !Dir["#{dir}/{Gemfile,*.gemspec}"].empty?
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/tasklib'
4
+ require 'thor/shell/color'
5
+
6
+ module Thermite
7
+ module Rails
8
+ module Tasks
9
+ # Base class to be used for creating Rake tasks for individual Projects.
10
+ class ProjectRakeTask < ::Rake::TaskLib
11
+ delegate :crate_name, :project_path, to: :@project
12
+
13
+ # @param project [Thermite::Rails::Project]
14
+ def initialize(project)
15
+ @project = project
16
+ @shell = Thor::Shell::Color.new
17
+ end
18
+
19
+ # @param [String]
20
+ def crate_name_for_ruby
21
+ crate_name.underscore
22
+ end
23
+
24
+ # @param [String]
25
+ def task_name
26
+ raise 'Define in child'
27
+ end
28
+
29
+ # In your child class, this method should `define` the Rake task.
30
+ def define_rake_task
31
+ return if Rake::Task.task_defined?(task_name)
32
+
33
+ raise 'Define in child'
34
+ end
35
+
36
+ # @return [Boolean] Has a task with this name already been defined?
37
+ def defined?
38
+ Rake::Task.task_defined?(task_name)
39
+ end
40
+
41
+ # @param message [String]
42
+ # @param color Whatever is supported by highline.
43
+ def color_puts(message, color = :blue)
44
+ @shell.say "[#{crate_name}] #{message}", color
45
+ end
46
+
47
+ private
48
+
49
+ # @param task_name [String]
50
+ def run_task(task_name)
51
+ color_puts("Running task: #{task_name}", :blue)
52
+
53
+ load_and do
54
+ Rake::Task[task_name].invoke
55
+ end
56
+ rescue RuntimeError => ex
57
+ if ex.message.match?("Don't know how to build task '#{task_name}'")
58
+ color_puts("#{ex.message}; skipping", :yellow)
59
+ else
60
+ abort ex.message
61
+ end
62
+ end
63
+
64
+ def load_and
65
+ Dir.chdir(project_path) do
66
+ load_rakefile do
67
+ yield
68
+ end
69
+ end
70
+ end
71
+
72
+ def load_rakefile
73
+ load 'Rakefile'
74
+ yield
75
+ rescue LoadError => ex
76
+ color_puts("Skipping due to LoadError: #{ex.message}", :red)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end