I have a complex Rails app and I want to extract some core functionality into an engine so that I can reuse the models etc in other Rails apps.
I've been following the official documentation for engines (https://guides.rubyonrails.org/engines.html). I'm able to create a new engine inside the app and generate some test models
> rails plugin new testengine --mountable
testengine> rails generate model Test
This is the .gemspec
require_relative "lib/testengine/version"
Gem::Specification.new do |spec|
spec.name = "testengine"
spec.version = Testengine::VERSION
spec.authors = ["Me"]
spec.summary = "testengine"
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
# to allow pushing to a single host or delete this section to allow pushing to any host.
spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
spec.files = Dir["{app,config,db,lib}/**/*", "Rakefile", "README.md"]
spec.add_dependency "rails", "~> 6.1.4"
end
I console into the test dummy rails app in testengine, and I can find my new model at Testengine::Test, no problem. So far so good.
Now I get to section 4.1 Mounting the Engine. I add the engine via the Gemfile file (in fact this is already done for me thanks to the rails generator above).
gem 'testengine', path: 'testengine'
Then I install my gems without problems.
> bundle install
...
Using testengine 0.1.0 from source at `testengine`
...
I console into the main app and I can find Testengine and Testengine::VERSION but not Testengine::Engine or Testengine::Test.
Reading a little further the docs say you need add this line to config/routes.rb
mount Testengine::Engine, at: "/testengine"
I do and now the rails app won't even start
config/routes.rb:3:in `block in <top (required)>': uninitialized constant Testengine::Engine (NameError)
What did I miss?
I will answer my question for the benefit of others who might make the same mistake I made. In my case, gem 'testengine', path: 'testengine' was buried inside a group of gems e.g. in
group :test do
...
end
I guess I was confused how rails loads gems from groups and missed the detail about group inclusion. It seems that while it'll show in the list during bundle install, and autoload some basic items, such as Testengine::VERSION it doesn't autoload everything unless you are running an environment as the same name as the group. In hindsight, this seems a bit obvious. Lesson learned.
Related
I am trying to run a project downloaded from internet, it is ruby on rails, I am getting the following error
Traceback (most recent call last):
4: from bin/rails:3:in `<main>'
3: from bin/rails:3:in `load'
2: from C:/Users/Amira/canvas/bin/spring:10:in `<top (required)>'
1: from C:/Users/Amira/canvas/bin/spring:10:in `read'
C:/Users/Amira/canvas/bin/spring:10:in `read': No such file or directory # rb_sysopen - C:/Users/Amira/canvas/Gemfile.lock (Errno::ENOENT)
The code written in the file is as following
#!/usr/bin/env ruby
# This file loads spring without using Bundler, in order to be fast.
# It gets overwritten when you run the `spring binstub` command.
unless defined?(Spring)
require 'rubygems'
require 'bundler'
if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
gem 'spring', match[1]
require 'spring/binstub'
end
end
It is an opensource project and I am new on rails
can anyone please help me ?
EDIT
Gemfile content
# What have they done to the Gemfile???
#
# Relax. Breathe deep. All the gems are still there; they're just loaded in various files in Gemfile.d/
# This allows us to require gems locally that we might not want to commit to our public repo. We can maintain
# a customized list of gems for development and debuggery, without affecting our ability to merge with canvas-lms
#
# NOTE: this file has to use 1.8.7 hash syntax to not raise a parser exception on 1.8.7
#
# NOTE: some files in Gemfile.d/ will have certain required gems indented. While this may seem arbitrary,
# it actually has semantic significance. An indented gem required in Gemfile is a gem that is NOT
# directly used by Canvas, but required by a gem that is used by Canvas. We lock into specific versions of
# these gems to prevent regression, and the indentation serves to alert us to the relationship between the gem and canvas-lms
source 'https://rubygems.org/'
Dir[File.join(File.dirname(__FILE__), 'gems/plugins/*/Gemfile.d/_before.rb')].each do |file|
eval(File.read(file), nil, file)
end
require File.expand_path("../config/canvas_rails_switcher", __FILE__)
Dir.glob(File.join(File.dirname(__FILE__), 'Gemfile.d', '*.rb')).sort.each do |file|
eval(File.read(file), nil, file)
end
The Gemfile usually specifies the dependencies (other code the application needs). The project you look into seems to use a slightly more complex setup.
Normally it works roughly like this:
Once the dependencies are installed via bundler (the command bundle or the more explicit bundle install), a Gemfile.lock is placed next to it to list the actually used versions of the installed dependencies.
Two takeaways:
use bundle (the gem is called bundler) to install dependencies.
read the README of the project carefully. If it does not have instructions on installation, create an issue if the projects has a method to do so. If you are hypernice or normal and the project is open and healthy, create a Pull Request for the README in which you describe how you managed to install the app.
Edit this might have sounded a bit arrogant, and I did not peek into the details. I assume you followed https://github.com/instructure/canvas-lms/wiki/Quick-Start . The canvas-lms project uses a setup slightly more complex than your typical Rails application. I believe you should ask specifically for help with canvas installation (edit your question).
If I call url_for within a feature spec, it returns an absolute URL starting with http://www.example.com/. Capybara will happily attempt to load pages on that site, but that has nothing to do with my app. Here are minimal steps to reproduce the issue:
Start with this Gemfile:
source 'https://rubygems.org'
gem "sqlite3"
gem "jquery-rails"
gem "draper"
gem "rails", '4.1.0'
gem "therubyracer"
gem "uglifier"
gem "rspec-rails"
gem "capybara"
gem "poltergeist"
gem "launchy"
Run the following:
bundle
rails new myapp -O
cd myapp
rm Gemfile Gemfile.lock
rails generate controller Test test
rails generate rspec:install
mkdir spec/features
Comment out the lines in spec/spec_helper.rb that say they should be removed when not using ActiveRecord, and then create spec/features/feature_spec.rb with the following contents:
require 'capybara/poltergeist'
Capybara.configure do |config|
config.javascript_driver = :poltergeist
end
require 'spec_helper'
describe "nothing", js: true do
specify do
visit(url_for(controller: :test, action: :test))
save_and_open_page
end
end
Finally, run rake spec and you will see the example.com page pop up in a browser. I have verified this behavior back to Rails 3.2.17.
Why is this happening, and is there a way to get URLs for the app being tested instead of example.com?
Edit: some things I have found looking into this more:
ActionDispatch::Routing::UrlFor.url_for is called from RSpec examples. It has only_path defaulting to false.
ActionView::RoutingUrlFor is the version you get in, say, a view. It has only_path defaulting to true, which works much better.
This commit to the rspec-rails gem probably caused the problem, by adding www.example.com as the default host. There is no explanation anywhere about why this host is an appropriate/useful choice.
The problem manifests for the following reasons:
You are using Poltergeist, which uses PhantomJS, which is perfectly capable of opening any URL.
You are using the url_for helper which needs to know the domain it should be generating a url for. When used inside a Rails view or controller, Rails supplies the domain based on what was used to make the request. When outside of a view or a controller, like in an ActionMailer or Capybara test, the domain is unknown. Capybara defaults the unknown domain to example.com.
So everything is working the way it should. Now, it happens to not be the way that you want it to work. However, if you want it to work how you would like you should do one of the following things:
Use the path_only option in url_for to tell it not to use the host part.
Use the host option in url_for to specify the correct host.
This is how those gems work. The http://example.com is irrelevant to your app. In general, you should not have fully hard-coded paths in your app. Rails attempts to determine your local domain (for specs this is example.com, which is configurable) and creates paths off of that.
The idea here is that you have a base URL which may change. Say, for staging I use a Heroku local app: randomname-123-staging.heroku.com. My urls will be prefixed with that. However, in production I own a domain name. My urls there will start with mydomain.com. It makes no sense for me to have to update all of my URLs based on the environments base domain; this should be (and is) provided by Rails.
By using a generic domain, which is supposed to be guaranteed to not resolve to a real IP, the specs help you code to this possibility.
I am developing a Rails app where most of the code not specific to the app has been written inside of various gems, including some Rails engines and some 3rd party gems for which I am enhancing or fixing bugs.
gem 'mygem', path: File.expath_path('../../mygem', __FILE__)
Since a lot of the code in these gems is really part of the app, it's still changing frequently. I'd like to be able to utilize the Rails feature where code is reloaded on each request when in development (i.e. when config.cache_classes is false), but this is only done within the normal application structure by default.
How can I configure Rails to reload gem code on each request, just like with the app code?
I have found through trial and error that several steps are required, with the help of ActiveSupport.
Add activesupport as a dependency in the .gemspec files
spec.add_dependency 'activesupport'
Include ActiveSupport::Dependencies in the top-level module of your gem (this was the most elusive requirement)
require 'bundler'; Bundler.setup
require 'active_support/dependencies'
module MyGem
unloadable
include ActiveSupport::Dependencies
end
require 'my_gem/version.rb'
# etc...
Set up your gem to use autoloading. You an either manually use ruby autoload declarations to map symbols into filenames, or use the Rails-style folder-structure-to-module-hierarchy rules (see ActiveSupport #constantize)
In each module and class in your gem, add unloadable.
module MyModule
unloadable
end
In each file that depends on a module or class from the gem, including in the gem itself, declare them at the top of each file using require_dependency. Look up the path of the gem as necessary to properly resolve the paths.
require_dependency "#{Gem.loaded_specs['my_gem'].full_gem_path}/lib/my_gem/myclass"
If you get exceptions after modifying a file and making a request, check that you haven't missed a dependency.
For some interesting details see this comprehensive post on Rails (and ruby) autoloading.
The solution that I used for Rails 6, with a dedicated Zeitwerk class loader and file checker :
Add the gem to the Rails project using the path: option in Gemfile
gem 'mygem', path: 'TODO' # The root directory of the local gem
In the development.rb, setup the classloader and the file watcher
gem_path = 'TODO' # The root directory of the local gem, the same used in Gemfile
# Create a Zeitwerk class loader for each gem
gem_lib_path = gem_path.join('lib').join(gem_path.basename)
gem_loader = Zeitwerk::Registry.loader_for_gem(gem_lib_path)
gem_loader.enable_reloading
gem_loader.setup
# Create a file watcher that will reload the gem classes when a file changes
file_watcher = ActiveSupport::FileUpdateChecker.new(gem_path.glob('**/*')) do
gem_loader.reload
end
# Plug it to Rails to be executed on each request
Rails.application.reloaders << Class.new do
def initialize(file_watcher)
#file_watcher = file_watcher
end
def updated?
#file_watcher.execute_if_updated
end
end.new(file_watcher)
With this, on each request, the class loader will reload the gem classes if one of them has been modified.
For a detailed walkthrough, see my article Embed a gem in a Rails project and enable autoreload.
I'm not sure if this sort of thing is very common, but I keep finding myself trying to create gems that are just wrappers around a Rails application.
My gem will have a generator to create a config.ru but the Rails application will live inside the gem's lib directory. I need to know how to "embed" a Rails application and configure it so that it can be run inside the gem.
For example:
$ mygem new project
mygem created a directory called "project" with the following files:
project/config.ru
project/widgets/
project/foobars/
My gem will also generate some directories that will need to be added to Rails somehow so that I can access the code in those directories from the Rails app living inside the Gem.
Any help or advice you can give me would be appreciated.
To clarify, I'm not trying to create a Rails engine, or plugin to a Rails application. I'm trying to create a fully-fledged Rails application, but package it as a gem so that a user of my gem can run the gem (the rails app) without needing to know that it's using Rails behind the scenes.
Update: Okay, I've got a little bit working now. I've created the gem and generated the rails project inside the gem's lib directory.
$ bundle gem my_gem && cd my_gem/lib
$ rails new my_gem --skip-bundle
Which leaves me with:
my_gem/
my_gem.gemspec
bin/my_gem
lib/
my_gem.rb
my_gem/
version.rb # generated by bundler
# the rails app:
app/
config/
Gemfile
...etc
Since this gem requires Rails, I started adding the gems defined in the Rails Gemfile as dependencies in the gem's Gemspec, but I'm a little confused as to how to handle the assets group in the Gemfile.
# Rails Gemfile
group :assets do
gem 'sass-rails', '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'
gem 'therubyracer', :platforms => :ruby
gem 'uglifier', '>= 1.0.3'
end
# gemspec
Gem::Specification.new do |gem|
gem.name = "my_gem"
# ...
gem.add_dependency 'rails', '3.2.8'
gem.add_dependency 'sqlite3'
gem.add_dependency 'jquery-rails'
# how to add the assets group gems?
end
Try this and see if it helps you make progress.
Gems are just directories of files, and you can put whatever files you want into a gem.
Create:
Create a blank gem full-blown Rails project:
$ bundle gem my_gem
Then a Rails app:
$ rails new my_app --skip-bundle
Copy the Rails files into the gem:
$ cp -R my_app/* my_gem
Bundle everything into your Rails app:
$ cd my_gem
$ bundle install --binstubs --path vendor/bundle
$ cd -
Make the Rakefile have the gem tasks and the Rails setup:
#!/usr/bin/env rake
require "bundler/gem_tasks"
require File.expand_path('../config/application', __FILE__)
MyApp::Application.load_tasks
Verify that it starts:
$ rails server
Load Path:
To control where Rails looks for files, such as "external" configuration files, you can use the file config/application.rb with any directory paths like this:
# Add additional load paths for your own custom dirs
# config.load_paths += %W( #{config.root}/../customdir )
Note the ".." which means go above the Rails directory. This gives you a path relative to the gem.
If you prefer you can specify an absolute path, for example if you know the user will always keep his external files in "~/myfiles/". You can also choose to use ENV vars to send in whatever directory you want.
If you read about load path capabilties, look for lines that are shorthand for adding a directory to the front of the load path because you may want to put your external diretories first:
$:.unshift File.dirname(__FILE__)
Gem Build:
Edit my_gem.gemspec to add your own description, homepage, summary, etc. then build:
$ gem build my_gem.gemspec
Successfully built RubyGem
Name: my_gem
Version: 0.0.1
File: my_gem-0.0.1.gem
Now your Rails app is packaged as a gem.
The config.ru should be a typical Rails one. No special changes AFAIK.
When your user wants to install your app:
$ gem install my_gem
The gem will install in the user's typical gem directory. If you want to adjust this, see this page on rubygems: http://docs.rubygems.org/read/chapter/3
Crate:
You may also want to investigate the Crate project:
Crate: Packaging Standalone Ruby Applications
http://www.slideshare.net/copiousfreetime/crate-packaging-standalone-ruby-applications
Rack:
To use config.ru here is the typical Rails setup:
# Rails.root/config.ru
require "config/environment"
use Rails::Rack::LogTailer
use ActionDispatch::Static
run ActionController::Dispatcher.new
For your project, you want to require some files before Rails. You'll want to learn about the Ruby "require" and how it finds files using LOAD_PATH.
The easy way:
# Rails.root/config.ru
require_relative 'filename'
require "config/environment"
Or to put the user's custom directory up couple directory levels:
require './../../filename' # not the best for security
Or to use an absolute path, read about File.expand_path:
File.expand_path(__FILE__)
Or to use the current directory and put it on the load path:
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'filename'
Lots of choices for you to consider. Hope this helps!
What about the question, "How am I going to run the Rails application inside the gem?".
A Rails application has controllers and views to run a web server. What you need are actions to create, list, update, and destroy. Exposing these actions without a web server is essentially having such methods in a class. That's a normal standard type of gem in the first place.
So maybe your questions is really, how do I write a gem where I have ActiveRecord, and the other Rails stuff.
First, you need to make your gem dependent on the Rails gems you need. You do this in the gemspec file for your gem.
Then it really is just a matter of your gem code doing a require of the right Rails gems you need.
I'm not sure if this will help, as I read through everything and I couldn't find the motivation behind why you were doing this. One of the reasons I came up with was making something that can be used on a desktop environment. In that case you could try using something like Bowline. If you just want to provide an application that others can download and use and install themselves, then you can probably assume they can follow at least basic developer kind of instructions and you could just provide the whole app on github or as a zip file. See an example of someone else doing something similar over on Fat Free CRM's github page.
In the Gemfile of my Rails project, I am starting to have auxiliary gems like "ruby-debug19", "perftools.rb", or "irbtools". All of these really have nothing to do with the project, but rather are part of my local development setup. But since I'm using bundler, I cannot load these gems (even though they are installed system-wide) unless I add them to the Gemfile. In my view that is a bit of a code smell.
For example, I would like to be able to require 'irbtools' in rails console without adding "irbtools" to my Gemfile.
Is there a way to keep auxiliary gems out of the Gemfile and still be able to load them for debugging, profiling, etc. when I need them?
Actually, you can create a group in you Gemfile like:
group :auxiliary do
gem 'irbtools'
end
And then use bundle install --without auxiliary if you don't want to use irbtools. Why do you think adding them to Gemfile is a code smell? And if it possible to do this without adding gems to the Gemfile it will be many more code smell I think.
Thanks to this post I have a great solution.
Add this line at the end of your Gemfile:
eval(File.read(File.dirname(__FILE__) + '/Gemfile.local'), binding) rescue nil
Create a file called Gemfile.local.
Add your development gems to Gemfile local. For example:
group :development do
gem 'cucumber'
end
Add Gemfile.local to .gitignore.
Now you can add your auxiliary development gems without changing the Gemfile for other folks on the team. Very cool.
I put the code below in a file in my app root, so it's easy to load from irb.
If you want it in something like a rails server, you probably need to add the load statement to environments/development.rb etc. That still creates problems if you accidentally check that in, but it's less annoying than having to add it to the Gemfile and causing your Gemfile.lock to change also.
# Example usage:
# add_more_gems("ruby-debug-base19-0.11.26", "linecache19-0.5.13")
# or
# add_more_gems(%w(ruby-debug-base19-0.11.26 linecache19-0.5.13))
#
# Note that you are responsible for:
# - adding all gem dependencies manually
# - making sure manually-added gem versions don't conflict with Gemfile.lock
# To list deps, run e.g. "gem dep ruby-debug-base19 -v 0.11.26"
#
def add_more_gems(*gem_names_and_vers)
gem_names_and_vers.flatten!
gem_base = File.expand_path(Gem.dir)
gem_names_and_vers.each do |gem_name_and_ver|
# uncomment if desired
###puts "Adding lib paths for #{gem_name_and_ver.inspect}"
spec_file = File.join(gem_base, 'specifications', "#{gem_name_and_ver}.gemspec")
spec = Gem::Specification.load spec_file
this_gem_dir = File.join(gem_base, 'gems', gem_name_and_ver)
spec.require_paths.each {|path|
dir_to_add = File.join(this_gem_dir, path)
$: << dir_to_add unless $:.member?(dir_to_add)
}
end
end
# put your often-used gems here
add_more_gems(
%w(
ruby-debug-base19-0.11.26
ruby-debug-ide19-0.4.12
linecache19-0.5.13
)
)
Not sure if this would work for you. It depends on whether or not you're using RVM. If you are, then you could install those auxiliary gems into the #global gemset that is created automatically for every Ruby interpreter. The gems in the #global gemset are available to all project-specific gemsets by default. This way you won't need to clutter up your Gemfiles.