Skip to content

Commit 32b531f

Browse files
committed
solve the rackup "chicken - egg" problem
with a plain Rack app when config.ru is evaluated it already assumes Rack to be loaded hovewer it's common practice to `bundle exec rackup` such scripts while Bundler loads a version of Rack that might be different than JRuby-Rack's bundled Rack version. thus for now we support a magic comment : # rack.version = ~>1.3.6 ... with "special" support for Bundler as well : # encoding: UTF-8 # rack.version = bundler ...
1 parent 1d3f185 commit 32b531f

File tree

4 files changed

+160
-8
lines changed

4 files changed

+160
-8
lines changed

src/main/java/org/jruby/rack/DefaultRackApplicationFactory.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public RackContext getRackContext() {
6060
public String getRackupScript() {
6161
return rackupScript;
6262
}
63+
64+
public void setRackupScript(String rackupScript) {
65+
this.rackupScript = rackupScript;
66+
this.rackupLocation = null;
67+
}
6368

6469
/**
6570
* Initialize this factory using the given context.
@@ -72,7 +77,7 @@ public void init(final RackContext rackContext) {
7277
// thus does not wrap exceptions into RackExceptions here ...
7378
// same applies for #newApplication() and #getApplication()
7479
this.rackContext = (ServletRackContext) rackContext;
75-
resolveRackupScript();
80+
if ( getRackupScript() == null ) resolveRackupScript();
7681
this.runtimeConfig = createRuntimeConfig();
7782
rackContext.log(RackLogger.INFO, runtimeConfig.getVersionString());
7883
configureDefaults();
@@ -152,6 +157,7 @@ public IRubyObject createApplicationObject(final Ruby runtime) {
152157
rackContext.log(RackLogger.WARN, "no rackup script found - starting empty Rack application!");
153158
rackupScript = "";
154159
}
160+
checkAndSetRackVersion(runtime);
155161
runtime.evalScriptlet("load 'jruby/rack/boot/rack.rb'");
156162
return createRackServletWrapper(runtime, rackupScript, rackupLocation);
157163
}
@@ -231,6 +237,31 @@ protected IRubyObject createRackServletWrapper(Ruby runtime, String rackup, Stri
231237
);
232238
}
233239

240+
String checkAndSetRackVersion(final Ruby runtime) {
241+
String rackVersion = null;
242+
try {
243+
rackVersion = IOHelpers.rubyMagicCommentValue(rackupScript, "rack.version:");
244+
}
245+
catch (Exception e) {
246+
rackContext.log(RackLogger.DEBUG, "could not read 'rack.version' magic comment from rackup", e);
247+
}
248+
if ( rackVersion == null ) {
249+
// NOTE: try matching a `require 'bundler/setup'` line ... maybe not ?!
250+
}
251+
if ( rackVersion != null ) {
252+
runtime.evalScriptlet("begin; require 'rubygems'; rescue LoadError; end");
253+
if ( rackVersion.equalsIgnoreCase("bundler") ) {
254+
runtime.evalScriptlet("require 'bundler/setup'");
255+
}
256+
else {
257+
rackContext.log(RackLogger.DEBUG, "detected 'rack.version' magic comment, " +
258+
"will use `gem 'rack', '"+ rackVersion +"'`");
259+
runtime.evalScriptlet("gem 'rack', '"+ rackVersion +"' if defined? gem");
260+
}
261+
}
262+
return rackVersion;
263+
}
264+
234265
static interface ApplicationObjectFactory {
235266
IRubyObject create(Ruby runtime) ;
236267
}

src/main/java/org/jruby/rack/util/IOHelpers.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
*/
2424
package org.jruby.rack.util;
2525

26+
import java.io.BufferedReader;
2627
import java.io.IOException;
2728
import java.io.InputStream;
2829
import java.io.InputStreamReader;
2930
import java.io.Reader;
31+
import java.io.StringReader;
3032
import java.util.regex.Matcher;
3133
import java.util.regex.Pattern;
3234

@@ -41,13 +43,12 @@ public static String inputStreamToString(final InputStream stream)
4143
throws IOException {
4244
if ( stream == null ) return null;
4345

44-
final StringBuilder str = new StringBuilder();
45-
int c = stream.read();
46-
Reader reader;
46+
final StringBuilder str = new StringBuilder(128);
4747
String coding = "UTF-8";
48-
if (c == '#') { // look for a coding: pragma
48+
int c = stream.read();
49+
if ( c == '#' ) { // look for a coding: pragma
4950
str.append((char) c);
50-
while ((c = stream.read()) != -1 && c != 10) {
51+
while ( (c = stream.read()) != -1 && c != 10 ) {
5152
str.append((char) c);
5253
}
5354
Pattern pattern = Pattern.compile("coding:\\s*(\\S+)");
@@ -58,13 +59,35 @@ public static String inputStreamToString(final InputStream stream)
5859
}
5960

6061
str.append((char) c);
61-
reader = new InputStreamReader(stream, coding);
62+
Reader reader = new InputStreamReader(stream, coding);
6263

63-
while ((c = reader.read()) != -1) {
64+
while ( (c = reader.read()) != -1 ) {
6465
str.append((char) c);
6566
}
6667

6768
return str.toString();
6869
}
70+
71+
public static String rubyMagicCommentValue(final String script, final String prefix)
72+
throws IOException {
73+
74+
final BufferedReader reader = new BufferedReader(new StringReader(script), 80);
75+
76+
String line, comment = null; Pattern pattern = null;
77+
while ( (line = reader.readLine()) != null ) {
78+
if ( line.charAt(0) == '#' ) {
79+
if (pattern == null) {
80+
pattern = Pattern.compile(prefix + "\\s*(\\S+)");
81+
}
82+
Matcher matcher = pattern.matcher(line);
83+
if (matcher.find()) {
84+
comment = matcher.group(1); break;
85+
}
86+
}
87+
else break; // (magic) comment expected at the beginning
88+
}
89+
reader.close();
90+
return comment;
91+
}
6992

7093
}

src/spec/ruby/rack/application_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#++
77

88
require File.expand_path('spec_helper', File.dirname(__FILE__) + '/..')
9+
require 'tempfile'
910

1011
describe org.jruby.rack.DefaultRackApplication, "call" do
1112

@@ -327,6 +328,52 @@ def newRuntime() # use the current runtime instead of creating new
327328
should_eval_as_nil "defined?(::Rack::VERSION)"
328329
end
329330

331+
it "loads specified version of rack", :lib => :stub do
332+
gem_install_rack_unless_installed '1.3.6'
333+
script = "" +
334+
"# rack.version: ~>1.3.6\n" +
335+
"Proc.new { 'proc-rack-app' }"
336+
app_factory.setRackupScript script
337+
@runtime = app_factory.new_runtime
338+
339+
app_factory.checkAndSetRackVersion(@runtime)
340+
@runtime.evalScriptlet "require 'rack'"
341+
342+
should_eval_as_eql_to "Rack.release if defined? Rack.release", '1.3'
343+
should_eval_as_eql_to "Gem.loaded_specs['rack'].version.to_s", '1.3.6'
344+
end
345+
346+
it "loads bundler with rack", :lib => :stub do
347+
gem_install_rack_unless_installed '1.3.6'
348+
script = "# encoding: UTF-8\n" +
349+
"# rack.version: bundler \n" +
350+
"Proc.new { 'proc-rack-app' }"
351+
app_factory.setRackupScript script
352+
@runtime = app_factory.new_runtime
353+
354+
file = Tempfile.new('Gemfile')
355+
file << "source 'http://rubygems.org'\n gem 'rack', '1.3.6'"
356+
file.flush
357+
@runtime.evalScriptlet "ENV['BUNDLE_GEMFILE'] = #{file.path.inspect}"
358+
359+
app_factory.checkAndSetRackVersion(@runtime)
360+
@runtime.evalScriptlet "require 'rack'"
361+
362+
should_not_eval_as_nil "defined?(::Bundler)"
363+
should_eval_as_eql_to "Rack.release if defined? Rack.release", '1.3'
364+
should_eval_as_eql_to "Gem.loaded_specs['rack'].version.to_s", '1.3.6'
365+
end
366+
367+
def gem_install_rack_unless_installed(version)
368+
begin
369+
Gem::Specification.find_by_name 'rack', version
370+
rescue Gem::LoadError
371+
require 'rubygems/dependency_installer'
372+
installer = Gem::DependencyInstaller.new
373+
installer.install 'rack', version
374+
end
375+
end
376+
330377
# should not matter on 1.7.x due https://github.com/jruby/jruby/pull/123
331378
if JRUBY_VERSION < '1.7.0'
332379
it "does not load any features (until load path is adjusted)" do

src/spec/ruby/rack/util_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# encoding: UTF-8
2+
#--
3+
# This source code is available under the MIT license.
4+
# See the file LICENSE.txt for details.
5+
#++
6+
7+
require File.expand_path('spec_helper', File.dirname(__FILE__) + '/..')
8+
9+
describe org.jruby.rack.util.IOHelpers do
10+
11+
IOHelpers = org.jruby.rack.util.IOHelpers
12+
13+
it "reads a stream into a string" do
14+
code = "# comment\n" +
15+
"puts 'vůl or kôň';\n" +
16+
"exit(0)\n"
17+
stream = java.io.ByteArrayInputStream.new code.to_java.getBytes('UTF-8')
18+
stream = java.io.BufferedInputStream.new(stream, 8)
19+
string = IOHelpers.inputStreamToString(stream)
20+
expect( string ).to eql "# comment\nputs 'vůl or kôň';\nexit(0)\n"
21+
end
22+
23+
# it "reads a stream into a string with encoding comment" do
24+
# code = "# encoding: ISO-8859-1\n" +
25+
# "# another comment \n" +
26+
# "puts 'vůl or kôň';\n"
27+
# #"puts 'v\xC5\xAFl or k\xC3\xB4\xC5\x88';\n"
28+
# bytes = java.lang.String.new(code.to_java.getBytes, "UTF-8").getBytes("ISO-8859-1")
29+
# stream = java.io.ByteArrayInputStream.new bytes
30+
# string = IOHelpers.inputStreamToString(stream)
31+
# expect( string ).to eql "# encoding: ISO-8859-1\n# another comment \nputs 'vůl or kôň';\n"
32+
# end
33+
34+
it "reads magic comment 1" do
35+
code = "# hello: world \n" +
36+
"# comment\n" +
37+
"exit(0);"
38+
string = IOHelpers.rubyMagicCommentValue(code, "hello:")
39+
expect( string ).to eql "world"
40+
end
41+
42+
it "reads magic comment 2" do
43+
code = "# encoding: UTF-8 \n" +
44+
"# comment\n" +
45+
"# rack.version: 1.3.6 \n" +
46+
"exit(0)\n'42'"
47+
string = IOHelpers.rubyMagicCommentValue(code, "rack.version:")
48+
expect( string ).to eql "1.3.6"
49+
end
50+
51+
end

0 commit comments

Comments
 (0)