warbler_updated 2.1.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.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/History.txt +411 -0
- data/LICENSE.txt +27 -0
- data/Mavenfile +32 -0
- data/README.rdoc +280 -0
- data/Rakefile +63 -0
- data/bin/warble +11 -0
- data/ext/JarMain.java +334 -0
- data/ext/WarMain.java +375 -0
- data/ext/WarblerJar.java +199 -0
- data/ext/WarblerJarService.java +18 -0
- data/lib/warbler/application.rb +104 -0
- data/lib/warbler/bundler_helper.rb +22 -0
- data/lib/warbler/config.rb +265 -0
- data/lib/warbler/executable_helper.rb +25 -0
- data/lib/warbler/gems.rb +77 -0
- data/lib/warbler/jar.rb +348 -0
- data/lib/warbler/pathmap_helper.rb +20 -0
- data/lib/warbler/platform_helper.rb +26 -0
- data/lib/warbler/rake_helper.rb +30 -0
- data/lib/warbler/scripts/rails.rb +5 -0
- data/lib/warbler/task.rb +185 -0
- data/lib/warbler/templates/bundler.erb +19 -0
- data/lib/warbler/templates/config.erb +1 -0
- data/lib/warbler/templates/jar.erb +11 -0
- data/lib/warbler/templates/jbundler.erb +2 -0
- data/lib/warbler/templates/rack.erb +5 -0
- data/lib/warbler/templates/rails.erb +1 -0
- data/lib/warbler/templates/war.erb +19 -0
- data/lib/warbler/traits/bundler.rb +157 -0
- data/lib/warbler/traits/gemspec.rb +79 -0
- data/lib/warbler/traits/jar.rb +58 -0
- data/lib/warbler/traits/jbundler.rb +48 -0
- data/lib/warbler/traits/nogemspec.rb +47 -0
- data/lib/warbler/traits/rack.rb +33 -0
- data/lib/warbler/traits/rails.rb +91 -0
- data/lib/warbler/traits/war.rb +260 -0
- data/lib/warbler/traits.rb +124 -0
- data/lib/warbler/version.rb +10 -0
- data/lib/warbler/war.rb +8 -0
- data/lib/warbler/web_server.rb +125 -0
- data/lib/warbler/zip_support.rb +13 -0
- data/lib/warbler.rb +40 -0
- data/lib/warbler_jar.jar +0 -0
- data/warble.rb +188 -0
- data/warbler.gemspec +37 -0
- data/web.xml.erb +57 -0
- metadata +188 -0
data/ext/WarMain.java
ADDED
@@ -0,0 +1,375 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2010-2012 Engine Yard, Inc.
|
3
|
+
* Copyright (c) 2007-2009 Sun Microsystems, Inc.
|
4
|
+
* This source code is available under the MIT license.
|
5
|
+
* See the file LICENSE.txt for details.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import java.lang.reflect.Method;
|
9
|
+
import java.io.InputStream;
|
10
|
+
import java.io.ByteArrayInputStream;
|
11
|
+
import java.io.SequenceInputStream;
|
12
|
+
import java.io.File;
|
13
|
+
import java.io.FileNotFoundException;
|
14
|
+
import java.io.FileOutputStream;
|
15
|
+
import java.net.URI;
|
16
|
+
import java.net.URLClassLoader;
|
17
|
+
import java.net.URL;
|
18
|
+
import java.util.Arrays;
|
19
|
+
import java.util.List;
|
20
|
+
import java.util.Properties;
|
21
|
+
import java.util.Map;
|
22
|
+
import java.util.jar.JarEntry;
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Used as a Main-Class in the manifest for a .war file, so that you can run
|
26
|
+
* a .war file with <tt>java -jar</tt>.
|
27
|
+
*
|
28
|
+
* WarMain can be used with different web server libraries. WarMain expects
|
29
|
+
* to have two files present in the .war file,
|
30
|
+
* <tt>WEB-INF/webserver.properties</tt> and <tt>WEB-INF/webserver.jar</tt>.
|
31
|
+
*
|
32
|
+
* When WarMain starts up, it extracts the webserver jar to a temporary
|
33
|
+
* directory, and creates a temporary work directory for the webapp. Both
|
34
|
+
* are deleted on exit.
|
35
|
+
*
|
36
|
+
* It then reads webserver.properties into a java.util.Properties object,
|
37
|
+
* creates a URL classloader holding the jar, and loads and invokes the
|
38
|
+
* <tt>main</tt> method of the main class mentioned in the properties.
|
39
|
+
*
|
40
|
+
* An example webserver.properties follows for Jetty. The <tt>args</tt>
|
41
|
+
* property indicates the names and ordering of other properties to be used
|
42
|
+
* as command-line arguments. The special tokens <tt>{{warfile}}</tt> and
|
43
|
+
* <tt>{{webroot}}</tt> are substituted with the location of the .war file
|
44
|
+
* being run and the temporary work directory, respectively.
|
45
|
+
* <pre>
|
46
|
+
* mainclass = org.eclipse.jetty.runner.Runner
|
47
|
+
* args = args0,args1,args2,args3,args4
|
48
|
+
* props = jetty.home
|
49
|
+
* args0 = --port
|
50
|
+
* args1 = {{port}}
|
51
|
+
* args2 = --config
|
52
|
+
* args3 = {{config}}
|
53
|
+
* args4 = {{warfile}}
|
54
|
+
* jetty.home = {{webroot}}
|
55
|
+
* </pre>
|
56
|
+
*
|
57
|
+
* System properties can also be set via webserver.properties. For example,
|
58
|
+
* the following entries set <tt>jetty.home</tt> before launching the server.
|
59
|
+
* <pre>
|
60
|
+
* props = jetty.home
|
61
|
+
* jetty.home = {{webroot}}
|
62
|
+
* </pre>
|
63
|
+
*/
|
64
|
+
public class WarMain extends JarMain {
|
65
|
+
|
66
|
+
static final String MAIN = '/' + WarMain.class.getName().replace('.', '/') + ".class";
|
67
|
+
static final String WEBSERVER_PROPERTIES = "/WEB-INF/webserver.properties";
|
68
|
+
static final String WEBSERVER_JAR = "/WEB-INF/webserver.jar";
|
69
|
+
static final String WEBSERVER_CONFIG = "/WEB-INF/webserver.xml";
|
70
|
+
static final String WEB_INF = "WEB-INF";
|
71
|
+
static final String META_INF = "META-INF";
|
72
|
+
|
73
|
+
/**
|
74
|
+
* jruby arguments, consider the following command :
|
75
|
+
* `java -jar rails.war --1.9 -S rake db:migrate`
|
76
|
+
* arguments == [ "--1.9" ]
|
77
|
+
* executable == "rake"
|
78
|
+
* executableArgv == [ "db:migrate" ]
|
79
|
+
*/
|
80
|
+
private final String[] arguments;
|
81
|
+
|
82
|
+
/**
|
83
|
+
* null to launch webserver or != null to run a executable e.g. rake
|
84
|
+
*/
|
85
|
+
private final String executable;
|
86
|
+
private final String[] executableArgv;
|
87
|
+
|
88
|
+
private File webroot;
|
89
|
+
|
90
|
+
WarMain(final String[] args) {
|
91
|
+
super(args);
|
92
|
+
final List<String> argsList = Arrays.asList(args);
|
93
|
+
final int sIndex = argsList.indexOf("-S");
|
94
|
+
if ( sIndex == -1 ) {
|
95
|
+
executable = null; executableArgv = null; arguments = null;
|
96
|
+
}
|
97
|
+
else {
|
98
|
+
if ( args.length == sIndex + 1 || args[sIndex + 1].isEmpty() ) {
|
99
|
+
throw new IllegalArgumentException("missing executable after -S");
|
100
|
+
}
|
101
|
+
arguments = argsList.subList(0, sIndex).toArray(new String[0]);
|
102
|
+
String execArg = argsList.get(sIndex + 1);
|
103
|
+
executableArgv = argsList.subList(sIndex + 2, argsList.size()).toArray(new String[0]);
|
104
|
+
|
105
|
+
if (execArg.equals("rails")) {
|
106
|
+
// The rails executable doesn't play well with ScriptingContainer, so we've packaged the
|
107
|
+
// same script that would have been generated by `rake rails:update:bin`
|
108
|
+
execArg = "./META-INF/rails.rb";
|
109
|
+
}
|
110
|
+
else if (execArg.equals("bundle") && executableArgv.length > 0 && executableArgv[0].equals("exec")) {
|
111
|
+
warn("`bundle exec' may drop out of the Warbler environment and into the system environment");
|
112
|
+
}
|
113
|
+
|
114
|
+
executable = execArg;
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
private URL extractWebserver() throws Exception {
|
119
|
+
this.webroot = File.createTempFile("warbler", "webroot");
|
120
|
+
this.webroot.delete();
|
121
|
+
this.webroot.mkdirs();
|
122
|
+
this.webroot = new File(this.webroot, new File(archive).getName());
|
123
|
+
debug("webroot directory is " + this.webroot.getPath());
|
124
|
+
InputStream jarStream = new URI("jar", entryPath(WEBSERVER_JAR), null).toURL().openStream();
|
125
|
+
File jarFile = File.createTempFile("webserver", ".jar");
|
126
|
+
jarFile.deleteOnExit();
|
127
|
+
FileOutputStream outStream = new FileOutputStream(jarFile);
|
128
|
+
try {
|
129
|
+
byte[] buf = new byte[4096];
|
130
|
+
int bytesRead;
|
131
|
+
while ((bytesRead = jarStream.read(buf)) != -1) {
|
132
|
+
outStream.write(buf, 0, bytesRead);
|
133
|
+
}
|
134
|
+
} finally {
|
135
|
+
jarStream.close();
|
136
|
+
outStream.close();
|
137
|
+
}
|
138
|
+
debug("webserver.jar extracted to " + jarFile.getPath());
|
139
|
+
return jarFile.toURI().toURL();
|
140
|
+
}
|
141
|
+
|
142
|
+
private Properties getWebserverProperties() throws Exception {
|
143
|
+
Properties props = new Properties();
|
144
|
+
try {
|
145
|
+
InputStream is = getClass().getResourceAsStream(WEBSERVER_PROPERTIES);
|
146
|
+
if ( is != null ) props.load(is);
|
147
|
+
} catch (Exception e) { }
|
148
|
+
|
149
|
+
String port = getSystemProperty("warbler.port", getENV("PORT"));
|
150
|
+
port = port == null ? "8080" : port;
|
151
|
+
String host = getSystemProperty("warbler.host", "0.0.0.0");
|
152
|
+
String webserverConfig = getSystemProperty("warbler.webserver_config", getENV("WARBLER_WEBSERVER_CONFIG"));
|
153
|
+
String embeddedWebserverConfig = new URI("jar", entryPath(WEBSERVER_CONFIG), null).toURL().toString();
|
154
|
+
webserverConfig = webserverConfig == null ? embeddedWebserverConfig : webserverConfig;
|
155
|
+
for ( Map.Entry entry : props.entrySet() ) {
|
156
|
+
String val = (String) entry.getValue();
|
157
|
+
val = val.replace("{{warfile}}", archive).
|
158
|
+
replace("{{port}}", port).
|
159
|
+
replace("{{host}}", host).
|
160
|
+
replace("{{config}}", webserverConfig).
|
161
|
+
replace("{{webroot}}", webroot.getAbsolutePath());
|
162
|
+
entry.setValue(val);
|
163
|
+
}
|
164
|
+
|
165
|
+
if (props.getProperty("props") != null) {
|
166
|
+
String[] propsToSet = props.getProperty("props").split(",");
|
167
|
+
for ( String key : propsToSet ) {
|
168
|
+
setSystemProperty(key, props.getProperty(key));
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
return props;
|
173
|
+
}
|
174
|
+
|
175
|
+
private void launchWebServer(URL jar) throws Exception {
|
176
|
+
URLClassLoader loader = new URLClassLoader(new URL[] {jar});
|
177
|
+
Thread.currentThread().setContextClassLoader(loader);
|
178
|
+
Properties props = getWebserverProperties();
|
179
|
+
String mainClass = props.getProperty("mainclass");
|
180
|
+
if (mainClass == null) {
|
181
|
+
throw new IllegalArgumentException("unknown webserver main class ("
|
182
|
+
+ WEBSERVER_PROPERTIES
|
183
|
+
+ " is missing 'mainclass' property)");
|
184
|
+
}
|
185
|
+
Class<?> klass = Class.forName(mainClass, true, loader);
|
186
|
+
Method main = klass.getDeclaredMethod("main", new Class[] { String[].class });
|
187
|
+
String[] newArgs = launchWebServerArguments(props);
|
188
|
+
debug("invoking webserver with: " + Arrays.deepToString(newArgs));
|
189
|
+
main.invoke(null, new Object[] { newArgs });
|
190
|
+
}
|
191
|
+
|
192
|
+
private String[] launchWebServerArguments(Properties props) {
|
193
|
+
String[] newArgs = args;
|
194
|
+
|
195
|
+
if (props.getProperty("args") != null) {
|
196
|
+
String[] insertArgs = props.getProperty("args").split(",");
|
197
|
+
newArgs = new String[args.length + insertArgs.length];
|
198
|
+
for (int i = 0; i < insertArgs.length; i++) {
|
199
|
+
newArgs[i] = props.getProperty(insertArgs[i], "");
|
200
|
+
}
|
201
|
+
System.arraycopy(args, 0, newArgs, insertArgs.length, args.length);
|
202
|
+
}
|
203
|
+
|
204
|
+
return newArgs;
|
205
|
+
}
|
206
|
+
|
207
|
+
// JarMain overrides to make WarMain "launchable"
|
208
|
+
// e.g. java -jar rails.war -S rake db:migrate
|
209
|
+
|
210
|
+
@Override
|
211
|
+
protected String getExtractEntryPath(final JarEntry entry) {
|
212
|
+
final String name = entry.getName();
|
213
|
+
final String res;
|
214
|
+
if ( name.startsWith(WEB_INF) ) {
|
215
|
+
// WEB-INF/app/controllers/application_controller.rb ->
|
216
|
+
// app/controllers/application_controller.rb
|
217
|
+
res = name.substring(WEB_INF.length());
|
218
|
+
} else if (name.startsWith(META_INF)) {
|
219
|
+
// Keep them where they are.
|
220
|
+
res = name;
|
221
|
+
} else {
|
222
|
+
// 404.html -> public/404.html
|
223
|
+
// javascripts -> public/javascripts
|
224
|
+
res = "/public/" + name;
|
225
|
+
}
|
226
|
+
return res;
|
227
|
+
}
|
228
|
+
|
229
|
+
@Override
|
230
|
+
protected URL extractEntry(final JarEntry entry, final String path) throws Exception {
|
231
|
+
// always extract but only return class-path entry URLs :
|
232
|
+
final URL entryURL = super.extractEntry(entry, path);
|
233
|
+
return path.endsWith(".jar") && path.startsWith("/lib/") ? entryURL : null;
|
234
|
+
}
|
235
|
+
|
236
|
+
@Override
|
237
|
+
protected int launchJRuby(final URL[] jars) throws Exception {
|
238
|
+
final Object scriptingContainer = newScriptingContainer(jars);
|
239
|
+
|
240
|
+
invokeMethod(scriptingContainer, "setArgv", (Object) executableArgv);
|
241
|
+
invokeMethod(scriptingContainer, "setCurrentDirectory", extractRoot.getAbsolutePath());
|
242
|
+
initJRubyScriptingEnv(scriptingContainer);
|
243
|
+
|
244
|
+
final Object provider = invokeMethod(scriptingContainer, "getProvider");
|
245
|
+
final Object rubyInstanceConfig = invokeMethod(provider, "getRubyInstanceConfig");
|
246
|
+
|
247
|
+
invokeMethod(rubyInstanceConfig, "setUpdateNativeENVEnabled", new Class[] { Boolean.TYPE }, false);
|
248
|
+
|
249
|
+
final CharSequence execScriptEnvPre = executableScriptEnvPrefix();
|
250
|
+
|
251
|
+
final String executablePath = locateExecutable(scriptingContainer, execScriptEnvPre);
|
252
|
+
if ( executablePath == null ) {
|
253
|
+
throw new IllegalStateException("failed to locate gem executable: '" + executable + "'");
|
254
|
+
}
|
255
|
+
invokeMethod(scriptingContainer, "setScriptFilename", executablePath);
|
256
|
+
|
257
|
+
invokeMethod(rubyInstanceConfig, "processArguments", (Object) arguments);
|
258
|
+
|
259
|
+
Object runtime = invokeMethod(scriptingContainer, "getRuntime");
|
260
|
+
|
261
|
+
debug("loading resource: " + executablePath);
|
262
|
+
Object executableInput =
|
263
|
+
new SequenceInputStream(new ByteArrayInputStream(execScriptEnvPre.toString().getBytes()),
|
264
|
+
(InputStream) invokeMethod(rubyInstanceConfig, "getScriptSource"));
|
265
|
+
|
266
|
+
debug("invoking " + executablePath + " with: " + Arrays.toString(executableArgv));
|
267
|
+
|
268
|
+
Object outcome = invokeMethod(runtime, "runFromMain",
|
269
|
+
new Class[] { InputStream.class, String.class },
|
270
|
+
executableInput, executablePath
|
271
|
+
);
|
272
|
+
return ( outcome instanceof Number ) ? ( (Number) outcome ).intValue() : 0;
|
273
|
+
}
|
274
|
+
|
275
|
+
@Deprecated
|
276
|
+
protected String locateExecutable(final Object scriptingContainer) throws Exception {
|
277
|
+
if ( executable == null ) {
|
278
|
+
throw new IllegalStateException("no executable");
|
279
|
+
}
|
280
|
+
final File exec = new File(extractRoot, executable);
|
281
|
+
if ( exec.exists() ) {
|
282
|
+
return exec.getAbsolutePath();
|
283
|
+
}
|
284
|
+
else {
|
285
|
+
final String script = locateExecutableScript(executable, executableScriptEnvPrefix());
|
286
|
+
return (String) invokeMethod(scriptingContainer, "runScriptlet", script);
|
287
|
+
}
|
288
|
+
}
|
289
|
+
|
290
|
+
protected String locateExecutable(final Object scriptingContainer, final CharSequence envPreScript)
|
291
|
+
throws Exception {
|
292
|
+
if ( executable == null ) {
|
293
|
+
throw new IllegalStateException("no executable");
|
294
|
+
}
|
295
|
+
final File exec = new File(extractRoot, executable);
|
296
|
+
if ( exec.exists() ) {
|
297
|
+
return exec.getAbsolutePath();
|
298
|
+
}
|
299
|
+
else {
|
300
|
+
final String script = locateExecutableScript(executable, envPreScript);
|
301
|
+
return (String) invokeMethod(scriptingContainer, "runScriptlet", script);
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
protected CharSequence executableScriptEnvPrefix() {
|
306
|
+
final String gemsDir = new File(extractRoot, "gems").getAbsolutePath();
|
307
|
+
final String gemfile = new File(extractRoot, "Gemfile").getAbsolutePath();
|
308
|
+
debug("setting GEM_HOME to " + gemsDir);
|
309
|
+
debug("... and BUNDLE_GEMFILE to " + gemfile);
|
310
|
+
|
311
|
+
// ideally this would look up the config.override_gem_home setting
|
312
|
+
return "ENV['GEM_HOME'] = ENV['GEM_PATH'] = '"+ gemsDir +"' \n" +
|
313
|
+
"ENV['BUNDLE_GEMFILE'] ||= '"+ gemfile +"' \n" +
|
314
|
+
"require 'uri:classloader:/META-INF/init.rb'";
|
315
|
+
}
|
316
|
+
|
317
|
+
protected String locateExecutableScript(final String executable, final CharSequence envPreScript) {
|
318
|
+
return ( envPreScript == null ? "" : envPreScript + " \n" ) +
|
319
|
+
"begin\n" + // locate the executable within gemspecs :
|
320
|
+
" require 'rubygems' unless defined?(Gem) \n" +
|
321
|
+
" begin\n" + // add bundled gems to load path :
|
322
|
+
" require 'bundler' \n" +
|
323
|
+
" rescue LoadError\n" + // bundler not used
|
324
|
+
" else\n" +
|
325
|
+
" env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] \n" + // init.rb sets ENV['RAILS_ENV'] ||= ...
|
326
|
+
" env ? Bundler.setup(:default, env) : Bundler.setup(:default) \n" +
|
327
|
+
" end if ENV_JAVA['warbler.bundler.setup'] != 'false' \n" + // java -Dwarbler.bundler.setup=false -jar my.war -S pry
|
328
|
+
" exec = '"+ executable +"' \n" +
|
329
|
+
" spec = Gem::Specification.find { |s| s.executables.include?(exec) } \n" +
|
330
|
+
" spec ? spec.bin_file(exec) : nil \n" +
|
331
|
+
// returns the full path to the executable
|
332
|
+
"rescue SystemExit => e\n" +
|
333
|
+
" e.status\n" +
|
334
|
+
"end";
|
335
|
+
}
|
336
|
+
|
337
|
+
protected void initJRubyScriptingEnv(Object scriptingContainer) throws Exception {
|
338
|
+
// for some reason, the container needs to run a scriptlet in order for it
|
339
|
+
// to be able to find the gem executables later
|
340
|
+
invokeMethod(scriptingContainer, "runScriptlet", "SCRIPTING_CONTAINER_INITIALIZED=true");
|
341
|
+
|
342
|
+
invokeMethod(scriptingContainer, "setHomeDirectory", "uri:classloader:/META-INF/jruby.home");
|
343
|
+
}
|
344
|
+
|
345
|
+
@Override
|
346
|
+
protected int start() throws Exception {
|
347
|
+
if ( executable == null ) {
|
348
|
+
try {
|
349
|
+
URL server = extractWebserver();
|
350
|
+
launchWebServer(server);
|
351
|
+
}
|
352
|
+
catch (FileNotFoundException e) {
|
353
|
+
final String msg = e.getMessage();
|
354
|
+
if ( msg != null && msg.contains("WEB-INF/webserver.jar") ) {
|
355
|
+
System.out.println("specify the -S argument followed by the bin file to run e.g. `java -jar rails.war -S rake -T` ...");
|
356
|
+
System.out.println("(or if you'd like your .war file to start a web server package it using `warbler executable war`)");
|
357
|
+
}
|
358
|
+
throw e;
|
359
|
+
}
|
360
|
+
return 0;
|
361
|
+
}
|
362
|
+
return super.start();
|
363
|
+
}
|
364
|
+
|
365
|
+
@Override
|
366
|
+
public void run() {
|
367
|
+
super.run();
|
368
|
+
if ( webroot != null ) delete(webroot.getParentFile());
|
369
|
+
}
|
370
|
+
|
371
|
+
public static void main(String[] args) {
|
372
|
+
doStart(new WarMain(args));
|
373
|
+
}
|
374
|
+
|
375
|
+
}
|
data/ext/WarblerJar.java
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2010-2012 Engine Yard, Inc.
|
3
|
+
* Copyright (c) 2007-2009 Sun Microsystems, Inc.
|
4
|
+
* This source code is available under the MIT license.
|
5
|
+
* See the file LICENSE.txt for details.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import java.io.Closeable;
|
9
|
+
import java.io.File;
|
10
|
+
import java.io.FileInputStream;
|
11
|
+
import java.io.FileNotFoundException;
|
12
|
+
import java.io.FileOutputStream;
|
13
|
+
import java.io.IOException;
|
14
|
+
import java.io.InputStream;
|
15
|
+
import java.util.regex.Matcher;
|
16
|
+
import java.util.regex.Pattern;
|
17
|
+
import java.util.zip.ZipEntry;
|
18
|
+
import java.util.zip.ZipInputStream;
|
19
|
+
import java.util.zip.ZipOutputStream;
|
20
|
+
|
21
|
+
import org.jruby.Ruby;
|
22
|
+
import org.jruby.RubyArray;
|
23
|
+
import org.jruby.RubyHash;
|
24
|
+
import org.jruby.RubyModule;
|
25
|
+
import org.jruby.RubyString;
|
26
|
+
import org.jruby.anno.JRubyMethod;
|
27
|
+
import org.jruby.runtime.Block;
|
28
|
+
import org.jruby.runtime.ThreadContext;
|
29
|
+
import org.jruby.runtime.builtin.IRubyObject;
|
30
|
+
import org.jruby.util.ByteList;
|
31
|
+
import org.jruby.util.JRubyFile;
|
32
|
+
|
33
|
+
public class WarblerJar {
|
34
|
+
public static void create(Ruby runtime) {
|
35
|
+
RubyModule task = runtime.getClassFromPath("Warbler::Jar");
|
36
|
+
task.defineAnnotatedMethods(WarblerJar.class);
|
37
|
+
}
|
38
|
+
|
39
|
+
@JRubyMethod
|
40
|
+
public static IRubyObject create_jar(ThreadContext context, IRubyObject self,
|
41
|
+
IRubyObject jar_path, IRubyObject entries) {
|
42
|
+
final Ruby runtime = context.runtime;
|
43
|
+
|
44
|
+
if (!(entries instanceof RubyHash)) {
|
45
|
+
throw runtime.newArgumentError("expected a hash for the second argument");
|
46
|
+
}
|
47
|
+
|
48
|
+
RubyHash hash = (RubyHash) entries;
|
49
|
+
try {
|
50
|
+
FileOutputStream file = newFile(jar_path);
|
51
|
+
try {
|
52
|
+
ZipOutputStream zip = new ZipOutputStream(file);
|
53
|
+
addEntries(context, zip, hash);
|
54
|
+
zip.finish();
|
55
|
+
} finally {
|
56
|
+
close(file);
|
57
|
+
}
|
58
|
+
} catch (IOException e) {
|
59
|
+
if (runtime.isDebug()) {
|
60
|
+
e.printStackTrace(runtime.getOut());
|
61
|
+
}
|
62
|
+
throw runtime.newIOErrorFromException(e);
|
63
|
+
}
|
64
|
+
|
65
|
+
return runtime.getNil();
|
66
|
+
}
|
67
|
+
|
68
|
+
@JRubyMethod
|
69
|
+
public static IRubyObject entry_in_jar(ThreadContext context, IRubyObject self,
|
70
|
+
IRubyObject jar_path, IRubyObject entry) {
|
71
|
+
final Ruby runtime = context.runtime;
|
72
|
+
try {
|
73
|
+
InputStream entryStream = getStream(jar_path.convertToString().getUnicodeValue(),
|
74
|
+
entry.convertToString().getUnicodeValue());
|
75
|
+
try {
|
76
|
+
byte[] buf = new byte[16384];
|
77
|
+
ByteList bytes = new ByteList();
|
78
|
+
int bytesRead;
|
79
|
+
while ((bytesRead = entryStream.read(buf)) != -1) {
|
80
|
+
bytes.append(buf, 0, bytesRead);
|
81
|
+
}
|
82
|
+
IRubyObject stringio = runtime.getModule("StringIO");
|
83
|
+
return stringio.callMethod(context, "new", runtime.newString(bytes));
|
84
|
+
} finally {
|
85
|
+
close(entryStream);
|
86
|
+
}
|
87
|
+
} catch (IOException e) {
|
88
|
+
if (runtime.isDebug()) {
|
89
|
+
e.printStackTrace(runtime.getOut());
|
90
|
+
}
|
91
|
+
throw runtime.newIOErrorFromException(e);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
private static void addEntries(ThreadContext context, ZipOutputStream zip, RubyHash entries) throws IOException {
|
96
|
+
RubyArray keys = entries.keys().sort(context, Block.NULL_BLOCK);
|
97
|
+
for (int i = 0; i < keys.getLength(); i++) {
|
98
|
+
IRubyObject key = keys.entry(i);
|
99
|
+
IRubyObject value = entries.op_aref(context, key);
|
100
|
+
addEntry(context, zip, key.convertToString().getUnicodeValue(), value);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
private static void addEntry(ThreadContext context, ZipOutputStream zip, String entryName, IRubyObject value) throws IOException {
|
105
|
+
if (value.respondsTo("read")) {
|
106
|
+
RubyString str = (RubyString) value.callMethod(context, "read").checkStringType();
|
107
|
+
ByteList strByteList = str.getByteList();
|
108
|
+
byte[] contents = strByteList.getUnsafeBytes();
|
109
|
+
zip.putNextEntry(new ZipEntry(entryName));
|
110
|
+
zip.write(contents, strByteList.getBegin(), strByteList.getRealSize());
|
111
|
+
} else {
|
112
|
+
File f;
|
113
|
+
if (value.isNil() || (f = getFile(value)).isDirectory()) {
|
114
|
+
zip.putNextEntry(new ZipEntry(entryName + "/"));
|
115
|
+
} else {
|
116
|
+
String path = f.getPath();
|
117
|
+
if (!f.exists()) {
|
118
|
+
path = value.convertToString().getUnicodeValue();
|
119
|
+
}
|
120
|
+
|
121
|
+
try {
|
122
|
+
InputStream inFile = getStream(path, null);
|
123
|
+
try {
|
124
|
+
zip.putNextEntry(new ZipEntry(entryName));
|
125
|
+
byte[] buf = new byte[16384];
|
126
|
+
int bytesRead;
|
127
|
+
while ((bytesRead = inFile.read(buf)) != -1) {
|
128
|
+
zip.write(buf, 0, bytesRead);
|
129
|
+
}
|
130
|
+
} finally {
|
131
|
+
close(inFile);
|
132
|
+
}
|
133
|
+
} catch (IOException e) {
|
134
|
+
System.err.println("File not found; " + path + " not in archive");
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
private static FileOutputStream newFile(IRubyObject jar_path) throws IOException {
|
141
|
+
return new FileOutputStream(getFile(jar_path));
|
142
|
+
}
|
143
|
+
|
144
|
+
private static File getFile(IRubyObject path) {
|
145
|
+
return JRubyFile.create(path.getRuntime().getCurrentDirectory(),
|
146
|
+
path.convertToString().getUnicodeValue());
|
147
|
+
}
|
148
|
+
|
149
|
+
private static void close(Closeable c) {
|
150
|
+
try {
|
151
|
+
c.close();
|
152
|
+
} catch (Exception e) {
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
private static final Pattern PROTOCOL = Pattern.compile("^[a-z][a-z0-9]+:");
|
157
|
+
|
158
|
+
private static InputStream getStream(String jar, String entry) throws IOException {
|
159
|
+
Matcher m = PROTOCOL.matcher(jar);
|
160
|
+
while (m.find()) {
|
161
|
+
jar = jar.substring(m.end());
|
162
|
+
m = PROTOCOL.matcher(jar);
|
163
|
+
}
|
164
|
+
|
165
|
+
String[] path = jar.split("!/");
|
166
|
+
InputStream stream = new FileInputStream(path[0]);
|
167
|
+
for (int i = 1; i < path.length; i++) {
|
168
|
+
stream = entryInJar(stream, path[i]);
|
169
|
+
}
|
170
|
+
|
171
|
+
if (entry == null) {
|
172
|
+
return stream;
|
173
|
+
}
|
174
|
+
|
175
|
+
return entryInJar(stream, entry);
|
176
|
+
}
|
177
|
+
|
178
|
+
private static String trimTrailingSlashes(String path) {
|
179
|
+
if (path.endsWith("/")) {
|
180
|
+
return path.substring(0, path.length() - 1);
|
181
|
+
} else {
|
182
|
+
return path;
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
private static InputStream entryInJar(InputStream jar, String entry) throws IOException {
|
187
|
+
entry = trimTrailingSlashes(entry);
|
188
|
+
|
189
|
+
ZipInputStream jstream = new ZipInputStream(jar);
|
190
|
+
ZipEntry zentry;
|
191
|
+
while ((zentry = jstream.getNextEntry()) != null) {
|
192
|
+
if (trimTrailingSlashes(zentry.getName()).equals(entry)) {
|
193
|
+
return jstream;
|
194
|
+
}
|
195
|
+
jstream.closeEntry();
|
196
|
+
}
|
197
|
+
throw new FileNotFoundException("entry '" + entry + "' not found in " + jar);
|
198
|
+
}
|
199
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2010-2012 Engine Yard, Inc.
|
3
|
+
* Copyright (c) 2007-2009 Sun Microsystems, Inc.
|
4
|
+
* This source code is available under the MIT license.
|
5
|
+
* See the file LICENSE.txt for details.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import java.io.IOException;
|
9
|
+
import org.jruby.Ruby;
|
10
|
+
import org.jruby.runtime.load.BasicLibraryService;
|
11
|
+
|
12
|
+
public class WarblerJarService implements BasicLibraryService {
|
13
|
+
public boolean basicLoad(final Ruby runtime) throws IOException {
|
14
|
+
WarblerJar.create(runtime);
|
15
|
+
return true;
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2010-2012 Engine Yard, Inc.
|
3
|
+
# Copyright (c) 2007-2009 Sun Microsystems, Inc.
|
4
|
+
# This source code is available under the MIT license.
|
5
|
+
# See the file LICENSE.txt for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
require 'rake'
|
9
|
+
|
10
|
+
# Extension of Rake::Application that allows the +warble+ command to
|
11
|
+
# report its name properly and inject its own tasks without a
|
12
|
+
# Rakefile.
|
13
|
+
class Warbler::Application < Rake::Application
|
14
|
+
include Warbler::RakeHelper
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
super
|
18
|
+
Warbler.application = self
|
19
|
+
@project_loaded = false
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the application name and loads Warbler's own tasks
|
23
|
+
def load_rakefile
|
24
|
+
@name = 'warble'
|
25
|
+
|
26
|
+
# Load the main warbler tasks
|
27
|
+
wt = Warbler::Task.new
|
28
|
+
|
29
|
+
task :default => wt.name
|
30
|
+
|
31
|
+
desc "Generate a configuration file to customize your archive"
|
32
|
+
task :config => "#{wt.name}:config"
|
33
|
+
|
34
|
+
desc "Install Warbler tasks in your Rails application"
|
35
|
+
task :pluginize => "#{wt.name}:pluginize"
|
36
|
+
|
37
|
+
desc "Feature: package gem repository inside a jar"
|
38
|
+
task :gemjar => "#{wt.name}:gemjar"
|
39
|
+
|
40
|
+
desc "Feature: make an executable archive (runnable + an embedded web server)"
|
41
|
+
task :executable => "#{wt.name}:executable"
|
42
|
+
|
43
|
+
desc "Feature: make a runnable archive (e.g. java -jar rails.war -S rake db:migrate)"
|
44
|
+
task :runnable => "#{wt.name}:runnable"
|
45
|
+
|
46
|
+
desc "Feature: precompile all Ruby files"
|
47
|
+
task :compiled => "#{wt.name}:compiled"
|
48
|
+
|
49
|
+
desc "Display version of Warbler"
|
50
|
+
task :version => "#{wt.name}:version"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Loads the project Rakefile in a separate application
|
54
|
+
def load_project_rakefile
|
55
|
+
return if @project_loaded
|
56
|
+
# Load any application rakefiles to aid in autodetecting applications
|
57
|
+
set_application Warbler.project_application = Rake::Application.new
|
58
|
+
Rake::Application::DEFAULT_RAKEFILES.each do |rf|
|
59
|
+
if File.exist?(rf)
|
60
|
+
begin
|
61
|
+
load rf
|
62
|
+
rescue LoadError
|
63
|
+
load File.join(Dir.getwd, rf)
|
64
|
+
end
|
65
|
+
break
|
66
|
+
end
|
67
|
+
end
|
68
|
+
set_application
|
69
|
+
@project_loaded = true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Run the application: The equivalent code for the +warble+ command
|
73
|
+
# is simply <tt>Warbler::Application.run</tt>.
|
74
|
+
def self.run; new.run end
|
75
|
+
|
76
|
+
# Run the application.
|
77
|
+
def run
|
78
|
+
set_application
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
# Remap the version option to display Warbler version.
|
83
|
+
def standard_rake_options
|
84
|
+
super.map do |opt|
|
85
|
+
if opt.first == '--version'
|
86
|
+
['--version', '-V', "Display the program version.",
|
87
|
+
lambda { |value|
|
88
|
+
puts "Warbler version #{Warbler::VERSION}"
|
89
|
+
exit
|
90
|
+
}
|
91
|
+
]
|
92
|
+
else
|
93
|
+
opt
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def set_application(app = self)
|
101
|
+
Rake.application = app
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|