Skip to content

Commit

Permalink
Introduce ./bin for your app's executables: bin/bundle, bin/rails, bi…
Browse files Browse the repository at this point in the history
…n/rake. Executable scripts are versioned code like the rest of your app. To generate a stub for a bundled gem: 'bundle binstubs unicorn' and 'git add bin/unicorn'
  • Loading branch information
jeremy committed Jan 7, 2013
1 parent 19b52d3 commit 009873a
Show file tree
Hide file tree
Showing 31 changed files with 163 additions and 230 deletions.
2 changes: 2 additions & 0 deletions guides/source/4_0_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railt

* New test locations `test/models`, `test/helpers`, `test/controllers`, and `test/mailers`. Corresponding rake tasks added as well. ([Pull Request](https://github.com/rails/rails/pull/7878))

* Your app's executables now live in the `bin/` dir. Run `rake update:bin` to get `bin/bundle`, `bin/rails`, and `bin/rake`.

* Threadsafe on by default

### Deprecations
Expand Down
2 changes: 1 addition & 1 deletion guides/source/action_controller_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ If you need a different session storage mechanism, you can change it in the `con
```ruby
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "script/rails g active_record:session_migration")
# (create the session table with "rails g active_record:session_migration")
# YourApp::Application.config.session_store :active_record_store
```

Expand Down
2 changes: 1 addition & 1 deletion guides/source/action_mailer_basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ Receiving and parsing emails with Action Mailer can be a rather complex endeavor

* Implement a `receive` method in your mailer.

* Configure your email server to forward emails from the address(es) you would like your app to receive to `/path/to/app/script/rails runner 'UserMailer.receive(STDIN.read)'`.
* Configure your email server to forward emails from the address(es) you would like your app to receive to `/path/to/app/bin/rails runner 'UserMailer.receive(STDIN.read)'`.

Once a method called `receive` is defined in any mailer, Action Mailer will parse the raw incoming email into an email object, decode it, instantiate a new mailer, and pass the email object to the mailer `receive` instance method. Here's an example:
Expand Down
4 changes: 2 additions & 2 deletions guides/source/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ Lastly, the `app/views` directory contains a `layouts` folder which contains a f

If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine.
#### `script` directory
#### `bin` directory
This directory contains one file, `script/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this:
This directory contains one file, `bin/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this:
```bash
rails g model
Expand Down
120 changes: 5 additions & 115 deletions guides/source/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,126 +26,16 @@ quickly.
Launch!
-------

A Rails application is usually started with the command `rails server`.
Now we finally boot and initialize the app. It all starts with your app's
`bin/rails` executable. A Rails application is usually started by running
`rails console` or `rails server`.

### `bin/rails`

The actual `rails` command is kept in _bin/rails_:

```ruby
#!/usr/bin/env ruby

if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git'))
railties_path = File.expand_path('../../lib', __FILE__)
$:.unshift(railties_path)
end
require "rails/cli"
```

This file will first attempt to push the `railties/lib` directory if
present, and then requires `rails/cli`.

### `railties/lib/rails/cli.rb`

This file looks like this:

```ruby
require 'rbconfig'
require 'rails/script_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::ScriptRailsLoader.exec_script_rails!

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
ARGV.shift
require 'rails/commands/plugin_new'
else
require 'rails/commands/application'
end
```

The `rbconfig` file from the Ruby standard library provides us with the `RbConfig` class which contains detailed information about the Ruby environment, including how Ruby was compiled. We can see this in use in `railties/lib/rails/script_rails_loader`.

```ruby
require 'pathname'

module Rails
module ScriptRailsLoader
RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
SCRIPT_RAILS = File.join('script', 'rails')
...

end
end
```

The `rails/script_rails_loader` file uses `RbConfig::Config` to obtain the `bin_dir` and `ruby_install_name` values for the configuration which together form the path to the Ruby interpreter. The `RbConfig::CONFIG["EXEEXT"]` will suffix this path with ".exe" if the script is running on Windows. This constant is used later on in `exec_script_rails!`. As for the `SCRIPT_RAILS` constant, we'll see that when we get to the `in_rails_application?` method.

Back in `rails/cli`, the next line is this:

```ruby
Rails::ScriptRailsLoader.exec_script_rails!
```

This method is defined in `rails/script_rails_loader`:

```ruby
def self.exec_script_rails!
cwd = Dir.pwd
return unless in_rails_application? || in_rails_application_subdirectory?
exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
Dir.chdir("..") do
# Recurse in a chdir block: if the search fails we want to be sure
# the application is generated in the original working directory.
exec_script_rails! unless cwd == Dir.pwd
end
rescue SystemCallError
# could not chdir, no problem just return
end
```

This method will first check if the current working directory (`cwd`) is a Rails application or a subdirectory of one. This is determined by the `in_rails_application?` method:

```ruby
def self.in_rails_application?
File.exists?(SCRIPT_RAILS)
end
```

The `SCRIPT_RAILS` constant defined earlier is used here, with `File.exists?` checking for its presence in the current directory. If this method returns `false` then `in_rails_application_subdirectory?` will be used:

```ruby
def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)
end
```

This climbs the directory tree until it reaches a path which contains a `script/rails` file. If a directory containing this file is reached then this line will run:

```ruby
exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
```

This is effectively the same as running `ruby script/rails [arguments]`, where `[arguments]` at this point in time is simply "server".

Rails Initialization
--------------------

Only now we finally start the real initialization process, beginning
with `script/rails`.

TIP: If you execute `script/rails` directly from your Rails app you will
skip executing all the code that we've just described.

### `script/rails`

This file is as follows:

```ruby
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require File.expand_path('../../config/boot', __FILE__)
require 'rails/commands'
Expand Down Expand Up @@ -227,7 +117,7 @@ If we used `s` rather than `server`, Rails will use the `aliases` defined in the
```ruby
when 'server'
# Change to the application's path if there is no config.ru file in current dir.
# This allows us to run script/rails server from other directories, but still get
# This allows us to run `rails server` from other directories, but still get
# the main config.ru and properly set the tmp directory.
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))

Expand Down
14 changes: 14 additions & 0 deletions railties/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
## Rails 4.0.0 (unreleased) ##

* App executables now live in the `bin/` directory: `bin/bundle`,
`bin/rails`, `bin/rake`. Run `rake rails:update:bin` to add these
executables to your own app. `script/rails` is gone from new apps.

Running executables within your app ensures they use your app's Ruby
version and its bundled gems, and it ensures your production deployment
tools only need to execute a single script. No more having to carefully
`cd` to the app dir and run `bundle exec ...`.

Rather than treating `bin/` as a junk drawer for generated "binstubs",
bundler 1.3 adds support for generating stubs for just the executables
you actually use: `bundle binstubs unicorn` generates `bin/unicorn`.
Add that executable to git and version it just like any other app code.

* `config.assets.enabled` is now true by default. If you're upgrading from a Rails 3.x app
that does not use the asset pipeline, you'll be required to add `config.assets.enabled = false`
to your application.rb. If you don't want the asset pipeline on a new app use `--skip-sprockets`
Expand Down
29 changes: 29 additions & 0 deletions railties/lib/rails/app_rails_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'pathname'

module Rails
module AppRailsLoader
RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
EXECUTABLE = 'bin/rails'

def self.exec_app_rails
cwd = Dir.pwd
return unless in_rails_application_or_engine? || in_rails_application_or_engine_subdirectory?
exec RUBY, EXECUTABLE, *ARGV if in_rails_application_or_engine?
Dir.chdir("..") do
# Recurse in a chdir block: if the search fails we want to be sure
# the application is generated in the original working directory.
exec_app_rails unless cwd == Dir.pwd
end
rescue SystemCallError
# could not chdir, no problem just return
end

def self.in_rails_application_or_engine?
File.exists?(EXECUTABLE) && File.read(EXECUTABLE) =~ /(APP|ENGINE)_PATH/
end

def self.in_rails_application_or_engine_subdirectory?(path = Pathname.new(Dir.pwd))
File.exists?(File.join(path, EXECUTABLE)) || !path.root? && in_rails_application_or_engine_subdirectory?(path.parent)
end
end
end
7 changes: 5 additions & 2 deletions railties/lib/rails/cli.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
require 'rbconfig'
require 'rails/script_rails_loader'
require 'rails/app_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::ScriptRailsLoader.exec_script_rails!
#
# TODO: when we hit this, advise adding ./bin to $PATH instead. Then the
# app's `rails` executable is run immediately.
Rails::AppRailsLoader.exec_app_rails

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }
Expand Down
2 changes: 1 addition & 1 deletion railties/lib/rails/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@

when 'server'
# Change to the application's path if there is no config.ru file in current dir.
# This allows us to run script/rails server from other directories, but still get
# This allows us to run `rails server` from other directories, but still get
# the main config.ru and properly set the tmp directory.
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))

Expand Down
2 changes: 1 addition & 1 deletion railties/lib/rails/commands/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/
opts.separator ""
opts.separator "You can also use runner as a shebang line for your scripts like this:"
opts.separator "You can also use runner as a shebang line for your executables:"
opts.separator "-------------------------------------------------------------"
opts.separator "#!/usr/bin/env #{File.expand_path($0)} runner"
opts.separator ""
Expand Down
2 changes: 1 addition & 1 deletion railties/lib/rails/generators/actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def generate(what, *args)
log :generate, what
argument = args.map {|arg| arg.to_s }.flatten.join(" ")

in_root { run_ruby_script("script/rails generate #{what} #{argument}", verbose: false) }
in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) }
end

# Runs the supplied rake task
Expand Down
22 changes: 11 additions & 11 deletions railties/lib/rails/generators/rails/app/app_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ def app
keep_file 'app/models/concerns'
end

def bin
directory "bin" do |content|
"#{shebang}\n" + content
end
chmod "bin", 0755, verbose: false
end

def config
empty_directory "config"

Expand Down Expand Up @@ -103,13 +110,6 @@ def public_directory
directory "public", "public", recursive: false
end

def script
directory "script" do |content|
"#{shebang}\n" + content
end
chmod "script", 0755, verbose: false
end

def test
empty_directory_with_keep_file 'test/fixtures'
empty_directory_with_keep_file 'test/controllers'
Expand Down Expand Up @@ -178,6 +178,10 @@ def create_app_files
build(:app)
end

def create_bin_files
build(:bin)
end

def create_config_files
build(:config)
end
Expand Down Expand Up @@ -211,10 +215,6 @@ def create_public_files
build(:public_directory)
end

def create_script_files
build(:script)
end

def create_test_files
build(:test) unless options[:skip_test_unit]
end
Expand Down
11 changes: 7 additions & 4 deletions railties/lib/rails/generators/rails/app/templates/README
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ The default directory structure of a generated Ruby on Rails application:
| | `-- concerns
| `-- views
| `-- layouts
|-- bin
|-- config
| |-- environments
| |-- initializers
Expand All @@ -177,7 +178,6 @@ The default directory structure of a generated Ruby on Rails application:
| `-- tasks
|-- log
|-- public
|-- script
|-- test
| |-- controllers
| |-- fixtures
Expand Down Expand Up @@ -226,6 +226,12 @@ app/helpers
generated for you automatically when using generators for controllers.
Helpers can be used to wrap functionality for your views into methods.

bin
Your app's executables -- bundler, rake, rails, and more -- automatically
run using your app's Ruby version and its bundled gems. When you bundle
a new gem and need to run one of its executables, use `bundle binstubs <gem>`
to add it. For example, `bundle binstubs unicorn` adds ./bin/unicorn.

config
Configuration files for the Rails environment, the routing map, the database,
and other dependencies.
Expand All @@ -248,9 +254,6 @@ public
default HTML files. This should be set as the DOCUMENT_ROOT of your web
server.

script
Helper scripts for automation and generation.

test
Unit and functional tests along with fixtures. When using the rails generate
command, template test files will be generated for you and placed in this
Expand Down
3 changes: 3 additions & 0 deletions railties/lib/rails/generators/rails/app/templates/bin/bundle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'rubygems'
load Gem.bin_path('bundler', 'bundle')
3 changes: 3 additions & 0 deletions railties/lib/rails/generators/rails/app/templates/bin/rails
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
3 changes: 3 additions & 0 deletions railties/lib/rails/generators/rails/app/templates/bin/rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require_relative '../config/boot'
require 'rake'
Rake.application.run

This file was deleted.

Loading

7 comments on commit 009873a

@xinuc
Copy link

@xinuc xinuc commented on 009873a Jan 7, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoaa..... 👿

@chrismo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this supposed to work with bundler's --binstubs turned on? The contents of bin/rails as a result of bundler vs. rails:update:bin are not the same, and the former won't start rails server properly. Heroku is also not able to start the rails server since this change, even pushing the bin folder up to it.

@jeremy
Copy link
Member Author

@jeremy jeremy commented on 009873a Jan 11, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chrismo It isn't. Bundler 1.3.0 will no longer overwrite executables in ./bin. We'll prefer to use bundler binstubs <gem> to explicitly add stubbed executables.

@chrismo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah - didn't know that, thx.

though browsing their commits just now, saw this one from 2 days ago -> rubygems/bundler@6540131 seems to undo that back to current (pre 1.3.0) behavior?

@chrismo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this appears to have been fixed -> rubygems/bundler#2253 -> rubygems/bundler@08138dc ... and with 1.3.0.pre7 it's no longer overwriting my local bin/rails.

@chrismo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and undone again in pre8 and now 1.3.0. rubygems/bundler@f00e085

https://gist.github.com/chrismo/5043420

I still may just be doing something wrong, but otherwise it seems there's some disagreement here on the tooling. Maybe --binstubs is just not a common use case?

EDIT: sorry - should have searched the issues more thoroughly -> "Use the new bundle binstubs command."

@jeremy
Copy link
Member Author

@jeremy jeremy commented on 009873a Feb 26, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a very common use case. We'd like to see that change. Try not using --binstubs anymore and start checking your ./bin/* in to version control.

Please sign in to comment.