jarbler 0.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 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: []