jarbler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bc9683c3fba322e4476646046c54b7c61ae7b6fa3d4c9865d1223fad0db590ee
4
+ data.tar.gz: 5e64e298302111c5fbcc606c8aad86ba93b454daece192318c33b3eca6696ccb
5
+ SHA512:
6
+ metadata.gz: 0edc420a59cb4e48364c0155c0cee0f2449b32264de8f3967cf790b82df541a367d7206b4a5dbf0461561be9e88fee79d8ba7bdc737d6ca2eda951a97cc5e0d9
7
+ data.tar.gz: 4e7d227903baacaaf8960f333c9f3beaee96dd5c399a45ab724620af04fbdfcbb5ed1ccce9a1955907d563ce2888b1d9a0b174dc2dadb85d557960e30ca7608c
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.2.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-04-12
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in jarbler.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ group(:test) do
11
+ gem 'minitest'
12
+ gem 'minitest-reporters'
13
+ # needed for minitests
14
+ gem 'jruby-jars'
15
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Peter Ramm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Jarbler
2
+ Pack a Ruby application into an executable jar file.
3
+
4
+ Jarbler allows you to create an self executing Java jar file containing your Ruby application.
5
+
6
+ This tool is inspired by the widely used jRuby runner Warbler.
7
+ In contrast to Warbler no Java servlet container is needed for execution.
8
+ Instead the configured executable is executed using the jRuby runtime jars.
9
+
10
+ ## Installation
11
+
12
+ Install the gem and add to the application's Gemfile by executing:
13
+
14
+ $ bundle add jarbler --group "development"
15
+
16
+ If bundler is not being used to manage dependencies, install the gem by executing:
17
+
18
+ $ gem install jarbler
19
+
20
+ ## Usage
21
+
22
+ To create a jar file simply run "jarble" in your application's root directory.
23
+
24
+ $ jarble
25
+
26
+ To adjust Jarbler's configuration, modify the settings in config file ´config/jarble.rb´. The template for this config file you create by executing
27
+
28
+ $ jarble config
29
+
30
+ ### Preconditions
31
+ * The Ruby app should be capable of running with jRuby
32
+ * Gems with native extensions should not be used (e.g. sassc)
33
+ * if needed for development or test such Gems with native extensions should be moved to the development and test group in the Gemfile.
34
+ * Otherwise the created jar file may not be executable on all platforms and Java versions.
35
+
36
+ ## Run the created jar file
37
+ The jar file created by Jarbler can be executed by
38
+
39
+ $ java -jar <jar filename>
40
+
41
+ Additional command line parameters are passed through to the executed Ruby app (like "-p 8900" for different network port number with bin/rails)
42
+
43
+ ## Configuration
44
+
45
+ The file config/jarble.rb contains the configuration for Jarbler.
46
+ To create a template config file with information about the supported configuration options, execute:
47
+
48
+ $ jarble config
49
+
50
+ The default configuration supports Ruby on Rails applications.<br>
51
+ The executable is set to "bin/rails" by default.<br>
52
+ The default executable parameters are "server -p 8080 -e production".
53
+
54
+ ## Troubleshooting
55
+ * Set DEBUG=true in environment to get additional runtime information
56
+
57
+ ## Contributing
58
+
59
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rammpeter/jarbler. <br>
60
+ Any feedback about usage experience or missing features is also appreciated.
61
+
62
+ ## License
63
+
64
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
5
+
6
+
7
+ require 'bundler/gem_tasks'
8
+
9
+ require 'rake/testtask'
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
data/bin/jarble ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'jarbler'
4
+
5
+ # call config if arguments are passed and argument = 'config'
6
+ # otherwise call Jarbler.run
7
+ if ARGV.empty?
8
+ Jarbler.run
9
+ else
10
+ if ARGV[0] == 'config' && ARGV.length == 1
11
+ Jarbler.config
12
+ else
13
+ puts "Invalid argument! Valid arguments are:"
14
+ puts "- no argument: build the jar"
15
+ puts "- config: create a template config file at #{Jarbler::Config::CONFIG_FILE}"
16
+ end
17
+ end
18
+
data/build_gem.sh ADDED
@@ -0,0 +1,18 @@
1
+ # Steps for creating gem
2
+ rake test
3
+ if [ $? -ne 0 ]; then
4
+ echo "Tests failed."
5
+ exit 1
6
+ fi
7
+
8
+ gem build
9
+ if [ $? -ne 0 ]; then
10
+ echo "Gem build failed."
11
+ exit 1
12
+ fi
13
+
14
+ gem install jarbler-0.1.0.gem
15
+ if [ $? -ne 0 ]; then
16
+ echo "Gem install failed."
17
+ exit 1
18
+ fi
data/jarbler.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/jarbler/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "jarbler"
7
+ spec.version = Jarbler::VERSION
8
+ spec.authors = ["Peter Ramm"]
9
+ spec.email = ["Peter@ramm-oberhermsdorf.de"]
10
+
11
+ spec.summary = "Create jar file from Rails app"
12
+ spec.description = "Pack a Rails application into an executable jar file"
13
+ spec.homepage = "https://github.com/rammpeter/jarbler"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/rammpeter/jarbler"
19
+ spec.metadata["changelog_uri"] = "https://github.com/rammpeter/jarbler/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
26
+ end
27
+ end
28
+ spec.bindir = "bin"
29
+ spec.executables << 'jarble'
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ # spec.add_dependency "example-gem", "~> 1.0"
34
+
35
+ # For more information and examples about making a new gem, check out our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
@@ -0,0 +1,209 @@
1
+ import java.io.File;
2
+ import java.io.FileInputStream;
3
+ import java.io.FileOutputStream;
4
+ import java.io.IOException;
5
+ import java.io.InputStream;
6
+ import java.util.Enumeration;
7
+ import java.util.UUID;
8
+ import java.util.jar.JarEntry;
9
+ import java.util.jar.JarFile;
10
+ import java.util.jar.JarInputStream;
11
+ import java.util.jar.JarOutputStream;
12
+ import java.util.ArrayList;
13
+ import java.util.Properties;
14
+ import java.util.zip.ZipEntry;
15
+ import java.util.zip.ZipInputStream;
16
+ import java.lang.reflect.Method;
17
+ import java.net.URL;
18
+ import java.net.URLClassLoader;
19
+ import java.lang.reflect.InvocationTargetException;
20
+ import java.lang.reflect.Constructor;
21
+ import java.net.MalformedURLException;
22
+ import java.lang.ClassNotFoundException;
23
+ import java.lang.NoSuchMethodException;
24
+ import java.lang.IllegalAccessException;
25
+ import java.lang.InstantiationException;
26
+
27
+
28
+ class JarMain {
29
+
30
+ // executed by java -jar <jar file name>
31
+ // No arguments are passed
32
+ public static void main(String[] args) {
33
+ debug("Start java process in jar file");
34
+ if (args.length > 0) {
35
+ debug("Java command line arguments are: ");
36
+ for (String arg : args) {
37
+ debug(" - " + arg);
38
+ }
39
+ }
40
+ // create a new folder in temp directory
41
+ File newFolder = new File(System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID().toString());
42
+ newFolder.mkdir();
43
+ try {
44
+ // Get the path of the jar file
45
+ String jarPath = JarMain.class.getProtectionDomain().getCodeSource().getLocation().getPath();
46
+
47
+ // remove the leading slash if path is a windows path
48
+ String os = System.getProperty("os.name").toLowerCase();
49
+ boolean isWindows = os.contains("windows");
50
+ if (os.contains("windows") && jarPath.startsWith("/") && jarPath.indexOf(':') != -1) {
51
+ jarPath = jarPath.substring(1); // remove the leading slash
52
+ }
53
+
54
+ // extract the jarFile by executing jar -xf jarFileName
55
+ System.out.println("Extracting files from "+jarPath+" to "+ newFolder.getAbsolutePath());
56
+ unzip(jarPath, newFolder.getAbsolutePath());
57
+
58
+
59
+ // get the file name of the jruby jar file in newFolder
60
+ File[] files = newFolder.listFiles();
61
+ // get the existing file from files where prefix is jruby-core* and suffix is .jar
62
+ File jrubyCoreFile = null;
63
+ File jrubyStdlibFile = null;
64
+ for (File file : files) {
65
+ if (file.getName().startsWith("jruby-core") && file.getName().endsWith(".jar")) {
66
+ jrubyCoreFile = file;
67
+ }
68
+ if (file.getName().startsWith("jruby-stdlib") && file.getName().endsWith(".jar")) {
69
+ jrubyStdlibFile = file;
70
+ }
71
+ }
72
+ debug("jruby core jar file is : "+ jrubyCoreFile.getAbsolutePath());
73
+ debug("jruby stdlib jar file is: "+ jrubyStdlibFile.getAbsolutePath());
74
+
75
+ // read the property file and get the port number
76
+ String executable = "Executable definition missing"; // Default if nothing else is specified
77
+ String executable_params = "";
78
+
79
+ Properties prop = new Properties();
80
+ prop.load(new FileInputStream(newFolder.getAbsolutePath()+File.separator+"jarbler.properties"));
81
+ executable = prop.getProperty("jarbler.executable");
82
+ executable_params = prop.getProperty("jarbler.executable_params");
83
+
84
+ // throw exception if executable is null
85
+ if (executable == null) {
86
+ throw new RuntimeException("Property 'executable' definition missing in jarbler.properties");
87
+ }
88
+
89
+ System.setProperty("GEM_PATH", newFolder.getAbsolutePath()+File.separator+"gems"); // not really necessray for Rails
90
+ System.setProperty("GEM_HOME", newFolder.getAbsolutePath()+File.separator+"gems"); // not really necessray for Rails
91
+ System.setProperty("BUNDLE_PATH", newFolder.getAbsolutePath()+File.separator+"gems"); // this drives bundler for rails app
92
+ System.setProperty("BUNDLE_WITHOUT", "test:development"); // exclude test and development dependencies from Gemfile check
93
+
94
+ // Load the Jar file
95
+ URLClassLoader classLoader = new URLClassLoader(new URL[]{
96
+ new URL("file://" + jrubyCoreFile.getAbsolutePath()),
97
+ new URL("file://" + jrubyStdlibFile.getAbsolutePath())
98
+ });
99
+
100
+ // Load the class
101
+ Class<?> clazz = classLoader.loadClass("org.jruby.Main");
102
+
103
+ // Get the method
104
+ Method mainMethod = clazz.getMethod("main", String[].class);
105
+
106
+ // Create an instance of the class
107
+ Constructor<?> constructor = clazz.getConstructor();
108
+ Object instance = (Object) constructor.newInstance();
109
+ //Object instance = clazz.newInstance();
110
+
111
+ // Prepare the argument list
112
+ ArrayList<String> mainArgs = new ArrayList<String>();
113
+ mainArgs.add(executable);
114
+ if (executable_params != null) {
115
+ for (String param : executable_params.split(" ")) {
116
+ mainArgs.add(param);
117
+ }
118
+ }
119
+ // add possible command line arguments
120
+ if (args.length > 0) {
121
+ for (String arg : args) {
122
+ mainArgs.add(arg);
123
+ }
124
+ }
125
+
126
+ debug("jRuby program starts with the following arguments: ");
127
+ for (String arg : mainArgs) {
128
+ debug(" - " + arg);
129
+ }
130
+
131
+ System.setProperty("user.dir", newFolder.getAbsolutePath()+File.separator+"app_root");
132
+ // call the method org.jruby.Main.main
133
+ mainMethod.invoke(null, (Object)mainArgs.toArray(new String[mainArgs.size()]));
134
+ } catch (Exception e) {
135
+ e.getCause().printStackTrace();
136
+ } finally {
137
+ // remove the temp directory newFolder
138
+ debug("jRuby program terminated, removing temporary folder "+ newFolder.getAbsolutePath());
139
+ deleteFolder(newFolder);
140
+ }
141
+ }
142
+
143
+ private static void unzip(String fileZip, String destination) throws IOException {
144
+ File destDir = new File(destination);
145
+
146
+ byte[] buffer = new byte[1024];
147
+ ZipInputStream zis = new ZipInputStream(new FileInputStream(fileZip));
148
+ ZipEntry zipEntry = zis.getNextEntry();
149
+ while (zipEntry != null) {
150
+ while (zipEntry != null) {
151
+ File newFile = newFile(destDir, zipEntry);
152
+ if (zipEntry.isDirectory()) {
153
+ if (!newFile.isDirectory() && !newFile.mkdirs()) {
154
+ throw new IOException("Failed to create directory " + newFile);
155
+ }
156
+ } else {
157
+ // fix for Windows-created archives
158
+ File parent = newFile.getParentFile();
159
+ if (!parent.isDirectory() && !parent.mkdirs()) {
160
+ throw new IOException("Failed to create directory " + parent);
161
+ }
162
+
163
+ // write file content
164
+ FileOutputStream fos = new FileOutputStream(newFile);
165
+ int len;
166
+ while ((len = zis.read(buffer)) > 0) {
167
+ fos.write(buffer, 0, len);
168
+ }
169
+ fos.close();
170
+ }
171
+ zipEntry = zis.getNextEntry();
172
+ }
173
+ }
174
+
175
+ zis.closeEntry();
176
+ zis.close();
177
+ }
178
+
179
+ private static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
180
+ File destFile = new File(destinationDir, zipEntry.getName());
181
+
182
+ String destDirPath = destinationDir.getCanonicalPath();
183
+ String destFilePath = destFile.getCanonicalPath();
184
+
185
+ if (!destFilePath.startsWith(destDirPath + File.separator)) {
186
+ throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
187
+ }
188
+
189
+ return destFile;
190
+ }
191
+
192
+ private static void debug(String msg) {
193
+ if (System.getenv("DEBUG") != null) {
194
+ System.out.println(msg);
195
+ }
196
+ }
197
+
198
+ private static void deleteFolder(File file){
199
+ for (File subFile : file.listFiles()) {
200
+ if(subFile.isDirectory()) {
201
+ deleteFolder(subFile);
202
+ } else {
203
+ subFile.delete();
204
+ }
205
+ }
206
+ file.delete();
207
+ }
208
+
209
+ }
@@ -0,0 +1,256 @@
1
+ require 'bundler'
2
+ require 'find'
3
+ require 'fileutils'
4
+
5
+ module Jarbler
6
+ class Builder
7
+ # Execute all functions needed to build the jar file
8
+ # Should be executed in application directory of Rails/Ruby application
9
+ # @return [void]
10
+ def build_jar
11
+ # create a temporary directory for staging
12
+ staging_dir = Dir.mktmpdir
13
+
14
+ jarbler_lib_dir = __dir__
15
+ app_root = Dir.pwd
16
+ debug "Project dir: #{app_root}"
17
+
18
+ # TODO: transform to internal bundler API call (check if jruby is installed + install if not)
19
+ exec_command "gem install --no-doc jruby-jars -v #{config.jruby_version}" # Ensure that jruby-jars are installed in the requested version
20
+ gem_search_locations = collect_gem_search_locations(app_root)
21
+ ruby_version = copy_jruby_jars_to_staging(staging_dir, gem_search_locations) # Copy the jruby jars to the staging directory
22
+ exec_command "javac -nowarn -Xlint:deprecation -source 8 -target 8 -d #{staging_dir} #{jarbler_lib_dir}/JarMain.java" # Compile the Java files
23
+
24
+ # Copy the application project to the staging directory
25
+ FileUtils.mkdir_p("#{staging_dir}/app_root")
26
+ config.includes.each do |dir|
27
+ file_utils_copy("#{app_root}/#{dir}", "#{staging_dir}/app_root") if File.exist?("#{app_root}/#{dir}")
28
+ end
29
+
30
+ # Get the needed Gems
31
+ raise "Gemfile.lock not found in #{app_root}" unless File.exist?("#{app_root}/Gemfile.lock")
32
+
33
+ gem_target_location = "#{staging_dir}/gems/jruby/#{ruby_version}"
34
+ FileUtils.mkdir_p("#{gem_target_location}/gems")
35
+ FileUtils.mkdir_p("#{gem_target_location}/specifications")
36
+
37
+ needed_gems = gem_dependencies # get the full names of the dependencies
38
+ needed_gems.each do |gem_full_name|
39
+ copy_gem_to_staging(gem_full_name, gem_target_location, gem_search_locations)
40
+ end
41
+
42
+ Dir.chdir(staging_dir) do
43
+ # create the manifest file
44
+ File.open('Manifest.txt', 'w') do |file|
45
+ file.write("Comment: created by Jarbler (https://github.com/rammpeter/jarbler)\n")
46
+ file.write("Main-Class: JarMain\n")
47
+ end
48
+
49
+ # Write java properties file for use in JarMain.java
50
+ File.open('jarbler.properties', 'w') do |file|
51
+ file.write("jarbler.executable=#{config.executable}\n")
52
+ # write a list of strings into property file delimited by space
53
+ java_executable_params = ''
54
+ config.executable_params.each do |param|
55
+ java_executable_params += "#{param} "
56
+ end
57
+ file.write("jarbler.executable_params=#{java_executable_params.strip}\n")
58
+ end
59
+
60
+ # remove files and directories from excludes, if they exist (after copying the rails project and the gems)
61
+ config.excludes.each do |exclude|
62
+ to_remove = "app_root/#{exclude}"
63
+ if File.exist?(to_remove)
64
+ debug "Removing #{to_remove} from staging directory"
65
+ FileUtils.rm_rf(to_remove)
66
+ else
67
+ debug "Not removing #{to_remove} from staging directory, because it does not exist"
68
+ end
69
+ end
70
+
71
+ exec_command "jar cfm #{config.jar_name} Manifest.txt *" # create the jar file
72
+
73
+ # place the jar in project directory
74
+ file_utils_copy(config.jar_name, app_root)
75
+ puts "Created jar file #{app_root}/#{config.jar_name}"
76
+ end
77
+
78
+ # remove temporary directory staging_dir
79
+ FileUtils.remove_entry staging_dir
80
+
81
+ end
82
+
83
+ private
84
+
85
+ # Find the locations where Gems are installed
86
+ # @param [String] app_root Application root directory
87
+ # @return [Array] Array of Gem locations
88
+ def collect_gem_search_locations(app_root)
89
+ # Search locations of gems in Gemfile.lock
90
+ possible_gem_search_locations = []
91
+ # Add possible local config first in search list
92
+ possible_gem_search_locations << bundle_config_bundle_path(app_root) if bundle_config_bundle_path(app_root)
93
+ ENV['GEM_PATH'].split(':').each do |gem_path|
94
+ possible_gem_search_locations << gem_path
95
+ end
96
+ debug "Possible Gem locations: #{possible_gem_search_locations}"
97
+ gem_search_locations = []
98
+ # Check where inside this location the gems may be installed
99
+ possible_gem_search_locations.each do |gem_search_location|
100
+ valid_gem_search_location = nil # No valid path found yet
101
+ Find.find(gem_search_location) do |path|
102
+ if File.directory?(path) && File.exist?("#{path}/specifications") && File.exist?("#{path}/gems")
103
+ valid_gem_search_location = path # Found a valid path
104
+ Find.prune # Do not search deeper
105
+ end
106
+ end
107
+ if valid_gem_search_location
108
+ gem_search_locations << valid_gem_search_location
109
+ else
110
+ debug "No valid gem location found in #{gem_search_location}"
111
+ end
112
+ end
113
+ debug "Valid Gem locations: #{gem_search_locations}"
114
+ gem_search_locations
115
+ end
116
+
117
+ # Copy the Gem elements to the staging directory
118
+ # @param [String] gem_full_name Full name of the Gem including version and platform
119
+ # @param [String] staging_dir Path to the staging directory
120
+ # @param [Array] gem_search_locations Array of Gem locations
121
+ # @return [void]
122
+ def copy_gem_to_staging(gem_full_name, gem_target_location, gem_search_locations)
123
+ gem_search_locations.each do |gem_search_location|
124
+ gem_dir = "#{gem_search_location}/gems/#{gem_full_name}"
125
+ if File.exist?(gem_dir)
126
+ file_utils_copy(gem_dir, "#{gem_target_location}/gems")
127
+ file_utils_copy("#{gem_search_location}/specifications/#{gem_full_name}.gemspec", "#{gem_target_location}/specifications")
128
+ return
129
+ end
130
+ end
131
+ raise "Gem #{gem_name} (#{gem_version}) not found in any of the following locations:\n#{gem_search_locations.join("\n")}"
132
+ end
133
+
134
+ # Read the default/production dependencies from Gemfile.lock and Gemfile
135
+ # @return [Array] Array with full names of dependencies
136
+ def gem_dependencies
137
+ needed_gems = []
138
+ lockfile_specs = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile)).specs
139
+
140
+ Bundler.setup # Load Gems specified in Gemfile
141
+ # filter Gems needed for production
142
+ gemfile_specs = Bundler.definition.dependencies.select do |d|
143
+ d.groups.include?(:default) || d.groups.include?(:production)
144
+ end
145
+
146
+ debug "Gems from Gemfile needed for production:"
147
+ gemfile_specs.each do |gemfile_spec|
148
+ # find lockfile record for Gemfile spec
149
+ lockfile_spec = lockfile_specs.find { |lockfile_spec| lockfile_spec.name == gemfile_spec.name }
150
+ if lockfile_spec
151
+ needed_gems << lockfile_spec.full_name unless needed_gems.include?(lockfile_spec.full_name)
152
+ debug "Direct Gem dependency: #{lockfile_spec.full_name}"
153
+ add_indirect_dependencies(lockfile_specs, lockfile_spec, needed_gems)
154
+ else
155
+ debug "Gem #{gemfile_spec.name} not found in Gemfile.lock"
156
+ end
157
+ end
158
+ needed_gems.uniq.sort
159
+ end
160
+
161
+ # recurively find all indirect dependencies
162
+ # @param [Array] lockfile_specs Array of Bundler::LockfileParser::Spec objects
163
+ # @param [Bundler::LockfileParser::Spec] lockfile_spec current lockfile spec to check for their dependencies
164
+ # @param [Array] needed_gems Array with full names of already found dependencies, add findings here
165
+ # @return [void]
166
+ def add_indirect_dependencies(lockfile_specs, lockfile_spec, needed_gems)
167
+ lockfile_spec.dependencies.each do |lockfile_spec_dep|
168
+ lockfile_spec_found = lockfile_specs.find { |lockfile_spec| lockfile_spec.name == lockfile_spec_dep.name }
169
+ if lockfile_spec_found
170
+ debug "Indirect Gem dependency from #{lockfile_spec.full_name}: #{lockfile_spec_found.full_name}"
171
+ unless needed_gems.include?(lockfile_spec_found.full_name)
172
+ needed_gems << lockfile_spec_found.full_name
173
+ add_indirect_dependencies(lockfile_specs, lockfile_spec_found, needed_gems)
174
+ end
175
+ else
176
+ debug "Gem #{lockfile_spec_dep.name} not found in Gemfile.lock"
177
+ end
178
+ end
179
+ end
180
+ def debug(msg)
181
+ puts msg if ENV['DEBUG']
182
+ end
183
+
184
+ def config
185
+ unless defined? @config
186
+ @config = Config.create
187
+ debug("Config attributes:")
188
+ @config.instance_variables.each do |var|
189
+ debug("#{var}: #{@config.instance_variable_get(var)}")
190
+ end
191
+ debug ""
192
+ end
193
+ @config
194
+ end
195
+
196
+ # Check if there is an additional local bundle path in .bundle/config
197
+ def bundle_config_bundle_path(rails_root)
198
+ bundle_path = nil # default
199
+ if File.exist?("#{rails_root}/.bundle/config")
200
+ bundle_config = YAML.load_file("#{rails_root}/.bundle/config")
201
+ if bundle_config && bundle_config['BUNDLE_PATH']
202
+ bundle_path = "#{rails_root}/#{bundle_config['BUNDLE_PATH']}"
203
+ debug "Local Gem path configured in #{rails_root}/.bundle/config: #{bundle_path}"
204
+ end
205
+ end
206
+ bundle_path
207
+ end
208
+
209
+ # Copy the jruby-jars to the staging directory
210
+ # @param [String] staging_dir Path to the staging directory
211
+ # @param [Array] gem_search_locations Array of Gem locations to look for jRuby jars
212
+ # @return [String] the ruby version of the jRuby jars
213
+ def copy_jruby_jars_to_staging(staging_dir, gem_search_locations)
214
+ jruby_jars_location = nil
215
+ gem_search_locations.each do |gem_search_location|
216
+ gem_dir = "#{gem_search_location}/gems/jruby-jars-#{config.jruby_version}"
217
+ if File.exist?(gem_dir)
218
+ jruby_jars_location = gem_dir
219
+ break
220
+ end
221
+ end
222
+ raise "Could not determine location of jRuby jars for release '#{config.jruby_version}' in the following output:\n#{lines}" unless jruby_jars_location
223
+ file_utils_copy("#{jruby_jars_location}/lib/jruby-core-#{config.jruby_version}-complete.jar", staging_dir)
224
+ file_utils_copy("#{jruby_jars_location}/lib/jruby-stdlib-#{config.jruby_version}.jar", staging_dir)
225
+
226
+ # Get the according Ruby version for the current jRuby version
227
+ lines = exec_command "java -cp #{jruby_jars_location}/lib/jruby-core-#{config.jruby_version}-complete.jar org.jruby.Main --version"
228
+ match_result = lines.match(/\(.*\)/)
229
+ raise "Could not determine Ruby version for jRuby #{config.jruby_version} in following output:\n#{lines}" unless match_result
230
+ ruby_version = match_result[0].tr('()', '')
231
+ debug "Corresponding Ruby version for jRuby (#{config.jruby_version}): #{ruby_version}"
232
+ ruby_version
233
+ end
234
+
235
+ # Execute the command and return the output
236
+ def exec_command(command)
237
+ lines = `#{command}`
238
+ raise "Command \"#{command}\"failed with return code #{$?} and output:\n#{lines}" unless $?.success?
239
+ debug "Command \"#{command}\" executed successfully with following output:\n#{lines}"
240
+ lines
241
+ end
242
+
243
+ # Copy file or directory with error handling
244
+ def file_utils_copy(source, destination)
245
+ if File.exist?(source) && File.directory?(source)
246
+ FileUtils.cp_r(source, destination)
247
+ else
248
+ FileUtils.cp(source, destination)
249
+ end
250
+ rescue Exception
251
+ puts "Error copying #{source} to #{destination}"
252
+ raise
253
+ end
254
+ end
255
+
256
+ end
@@ -0,0 +1,108 @@
1
+ module Jarbler
2
+ class Config
3
+ attr_accessor :jar_name, :includes, :excludes, :jruby_version, :executable, :executable_params
4
+
5
+ CONFIG_FILE = 'config/jarble.rb'
6
+ # create instence of Config class with defaults or from config file
7
+ # Should be called from rails/ruby root directory
8
+ def self.create
9
+ if File.exist?(CONFIG_FILE)
10
+ config = eval(File.read(CONFIG_FILE), binding, CONFIG_FILE, 0)
11
+ else
12
+ config = Jarbler::Config.new
13
+ end
14
+ unless config.class == Jarbler::Config
15
+ Jarbler.debug "No valid config provided in #{CONFIG_FILE}! Using defaults."
16
+ config = Config.new
17
+ end
18
+ config.define_jruby_version
19
+ config
20
+ end
21
+
22
+ def initialize
23
+ @jar_name = File.basename(Dir.pwd) + '.jar'
24
+ @includes = %w(app bin config config.ru db Gemfile Gemfile.lock lib log public Rakefile script vendor tmp)
25
+ @excludes = %w(tmp/cache tmp/pids tmp/sockets vendor/bundle vendor/cache vendor/ruby)
26
+ @jruby_version = nil # determined automatically at runtime
27
+ @executable = 'bin/rails'
28
+ @executable_params = %w(server -e production -p 8080)
29
+ # execute additional block if given
30
+ yield self if block_given?
31
+ end
32
+
33
+ # Generate the template config file based on default values
34
+ def create_config_file
35
+ write_config_file("\
36
+ # Name of the generated jar file
37
+ # config.jar_name = '#{jar_name}'
38
+
39
+ # Application directories or files to include in the jar file
40
+ # config.includes = #{includes}
41
+ # config.includes << 'additional'
42
+
43
+ # Application directories or files to exclude from the jar file
44
+ # config.excludes = #{excludes}
45
+ # config.excludes << 'additional'
46
+
47
+ # Use certail jRuby version
48
+ # if not set (nil) then the version defined in .ruby-version
49
+ # if not jRuby version defined here or in .ruby-version then the latest available jRuby version is used
50
+ # config.jruby_version = '9.2.3.0'
51
+ # config.jruby_version = nil
52
+
53
+ # The Ruby executable file to run, e.g. 'bin/rails' or 'bin/rake'
54
+ # config.executable = '#{executable}'
55
+
56
+ # Additional command line parameters for the Ruby executable
57
+ # config.executable_params = #{executable_params}
58
+ ".split("\n"))
59
+ end
60
+
61
+ # write a config file with the given lines
62
+ # if the file exists, it is overwritten
63
+ # if the file does not exist, it is created
64
+ # @param [Array] lines is an array of strings
65
+ def write_config_file(lines)
66
+ lines = [lines] unless lines.is_a?(Array)
67
+ FileUtils.mkdir_p('config')
68
+ raise "config file #{CONFIG_FILE} already exists in current directory! Please move file temporary and try again." if File.exist?(CONFIG_FILE)
69
+ File.open(CONFIG_FILE, 'w') do |file|
70
+ file.write("# Jarbler configuration, see https://github.com/rammpeter/jarbler\n")
71
+ file.write("# values in comments are the default values\n")
72
+ file.write("# uncomment and adjust if needed\n")
73
+ file.write(" \n")
74
+ file.write("Jarbler::Config.new do |config|\n")
75
+ lines.each do |line|
76
+ file.write(" #{line}\n")
77
+ end
78
+ file.write("end\n")
79
+ end
80
+ puts "Jarbler: Created config file #{Dir.pwd}/#{CONFIG_FILE}"
81
+ end
82
+
83
+ # define jRuby version if not set in config file
84
+ def define_jruby_version
85
+ unless @jruby_version # not defined in config file
86
+ if File.exist?('.ruby-version')
87
+ # read the file RAILS_ROOT/.ruby-version starting from char at position 6 to the end of the line
88
+ self.jruby_version = File.read('.ruby-version')[6..20].strip
89
+ debug "jRuby version from .ruby-version file: #{jruby_version}"
90
+ else
91
+ # no .ruby-version file, use jRuby version of the latest Gem
92
+ # Fetch the gem specification from Rubygems.org
93
+ command = "gem search --remote jruby-jars"
94
+ lines = `#{command}`
95
+ raise "Command \"#{command}\" failed with return code #{$?} and output:\n#{lines}" unless $?.success?
96
+ jruby_jars_line = lines.match(/^jruby-jars \((.*)\)/)
97
+ raise "No jruby-jars gem found in rubygems.org!" unless jruby_jars_line
98
+ self.jruby_version = /\((.*?)\)/.match(jruby_jars_line.to_s)[1]
99
+ debug "jRuby version from latest jruby-jars gem: #{jruby_version}"
100
+ end
101
+ end
102
+ end
103
+
104
+ def debug(msg)
105
+ puts msg if ENV['DEBUG']
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jarbler
4
+ VERSION = "0.1.0"
5
+ VERSION_DATE = "2023-04-20"
6
+ end
data/lib/jarbler.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+ require 'yaml'
6
+ require 'bundler'
7
+ require 'bundler/lockfile_parser'
8
+
9
+ require_relative "jarbler/version"
10
+ require_relative "jarbler/builder"
11
+ require_relative "jarbler/config"
12
+
13
+
14
+ module Jarbler
15
+ def self.run
16
+ puts "Jarbler release #{VERSION}, #{VERSION_DATE} ( https://github.com/rammpeter/jarbler )"
17
+ Builder.new.build_jar
18
+ end
19
+
20
+ def self.config
21
+ Jarbler::Config.new.create_config_file
22
+ end
23
+ end
data/sig/jarbler.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Jarbler
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jarbler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Ramm
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Pack a Rails application into an executable jar file
14
+ email:
15
+ - Peter@ramm-oberhermsdorf.de
16
+ executables:
17
+ - jarble
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".ruby-version"
22
+ - CHANGELOG.md
23
+ - Gemfile
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - bin/jarble
28
+ - build_gem.sh
29
+ - jarbler.gemspec
30
+ - lib/jarbler.rb
31
+ - lib/jarbler/JarMain.java
32
+ - lib/jarbler/builder.rb
33
+ - lib/jarbler/config.rb
34
+ - lib/jarbler/version.rb
35
+ - sig/jarbler.rbs
36
+ homepage: https://github.com/rammpeter/jarbler
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ homepage_uri: https://github.com/rammpeter/jarbler
41
+ source_code_uri: https://github.com/rammpeter/jarbler
42
+ changelog_uri: https://github.com/rammpeter/jarbler/CHANGELOG.md
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.6.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.4.12
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Create jar file from Rails app
62
+ test_files: []