furoshiki 0.2.0 → 0.3.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.ruby-version +1 -1
  4. data/Gemfile +0 -1
  5. data/Rakefile +5 -0
  6. data/furoshiki.gemspec +1 -1
  7. data/lib/furoshiki/configuration.rb +142 -0
  8. data/lib/furoshiki/jar.rb +41 -0
  9. data/lib/furoshiki/jar_app.rb +225 -0
  10. data/lib/furoshiki/util.rb +27 -0
  11. data/lib/furoshiki/validator.rb +42 -0
  12. data/lib/furoshiki/version.rb +1 -1
  13. data/lib/furoshiki/warbler_extensions.rb +12 -0
  14. data/lib/furoshiki.rb +4 -3
  15. data/lib/warbler/traits/furoshiki.rb +31 -0
  16. data/spec/{shoes/swt_app_spec.rb → app_spec.rb} +29 -22
  17. data/spec/configuration_spec.rb +12 -0
  18. data/spec/fixtures/config.yaml +1 -0
  19. data/spec/{shoes → fixtures}/test_app/app.yaml +0 -0
  20. data/spec/{shoes → fixtures}/test_app/bin/hello_world +0 -0
  21. data/spec/{shoes → fixtures}/test_app/dir_to_ignore/file_to_ignore.txt +0 -0
  22. data/spec/{shoes → fixtures}/test_app/img/boots.icns +0 -0
  23. data/spec/{shoes → fixtures}/test_app/img/boots.ico +0 -0
  24. data/spec/{shoes → fixtures}/test_app/img/boots_512x512x32.png +0 -0
  25. data/spec/{shoes → fixtures}/test_app/sibling.rb +0 -0
  26. data/spec/fixtures/test_project/bin/hello.rb +1 -0
  27. data/spec/fixtures/test_project/dir_to_ignore/file_to_ignore.txt +1 -0
  28. data/spec/{shoes/swt_jar_spec.rb → jar_spec.rb} +25 -12
  29. data/spec/spec_helper.rb +51 -0
  30. data/spec/support/shared_config.rb +19 -0
  31. data/spec/support/shared_zip.rb +1 -1
  32. data/spec/util_spec.rb +57 -0
  33. metadata +73 -45
  34. data/lib/furoshiki/shoes/configuration.rb +0 -184
  35. data/lib/furoshiki/shoes/swt_app.rb +0 -230
  36. data/lib/furoshiki/shoes/swt_jar.rb +0 -67
  37. data/lib/furoshiki/shoes.rb +0 -15
  38. data/lib/warbler/traits/shoes.rb +0 -51
  39. data/spec/shoes/configuration_spec.rb +0 -198
  40. data/spec/shoes/spec_helper.rb +0 -70
  41. data/spec/shoes/support/shared_config.rb +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d0a17813563822aee7e3ff59e3ed2fd2751ee933
4
- data.tar.gz: eea188ad94a3bbfa03fbe232f8b6e8200dcaa85f
3
+ metadata.gz: 28682823686b0e99c68d8e89e8dda278cedcbd7f
4
+ data.tar.gz: 35eaf912683fd1e7f183d21cf55f998652f04348
5
5
  SHA512:
6
- metadata.gz: 42e0d0a9be95baf3ff80bda2606708f8240d43726fff18dc6165841278e6b8e1aa6d1f675ccd813e5813be076c4aa3f5a8a9ee18a9141379f980002d4876b19b
7
- data.tar.gz: f48f91a79590f621aaaed9ddef6dccb6e0999f9462453345087cd64b64fb3e3ad4206e05934a0caf33761173230567e5d63ebe51de4c95d367de19a45d667da5
6
+ metadata.gz: 98811a3dfc69cda04149f26361b3b31d7b8525bb79b7151db8bba034c88e4468e93ebff266f9472026741c27b2ca53b72179c015822bee0c16461b5060aa4cc0
7
+ data.tar.gz: dec72fe9319649dfb226b22946b3bb2678443b0e83a8f39411e02b346d4d805c639d22d69b9fc678eb7aa931d78b89606deb906c12f6058e9f759e550ce88c00
data/.gitignore CHANGED
@@ -21,4 +21,4 @@ pkg
21
21
  Gemfile.lock
22
22
 
23
23
  ## PROJECT::SPECIFIC
24
- spec/shoes/.furoshiki
24
+ spec/.furoshiki
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- jruby-1.7.16
1
+ jruby-1.7.18
data/Gemfile CHANGED
@@ -2,4 +2,3 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'shoes'
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'bundler'
2
2
  require 'rake/clean'
3
+ require 'rspec/core/rake_task'
3
4
  Bundler::GemHelper.install_tasks
4
5
 
5
6
  @types = %w[rb]
@@ -19,3 +20,7 @@ namespace :whitespace do
19
20
  end
20
21
 
21
22
  CLEAN.include('spec/shoes/.furoshiki', 'spec/shoes/sample_app')
23
+
24
+ RSpec::Core::RakeTask.new(:spec)
25
+
26
+ task :default => :spec
data/furoshiki.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_dependency 'rubyzip', '>= 1.0.0'
22
22
 
23
23
  s.add_development_dependency 'rake'
24
- s.add_development_dependency 'rspec'
24
+ s.add_development_dependency 'rspec', '>= 3.0.0'
25
25
  s.add_development_dependency 'rspec-its'
26
26
  s.add_development_dependency 'pry'
27
27
  end
@@ -0,0 +1,142 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+ require 'furoshiki/validator'
4
+ require 'furoshiki/warbler_extensions'
5
+ require 'furoshiki/util'
6
+
7
+ module Furoshiki
8
+ # Configuration for Furoshiki packagers.
9
+ #
10
+ # If your configuration uses hashes, the keys will always be
11
+ # symbols, even if you have created it with string keys. It's just
12
+ # easier that way.
13
+ #
14
+ # This is a value object. If you need to modify your configuration
15
+ # after initialization, dump it with #to_hash, make your changes,
16
+ # and instantiate a new object.
17
+ class Configuration
18
+ JAR_APP_TEMPLATE_URL = 'https://s3.amazonaws.com/net.wasnotrice.shoes/wrappers/shoes-app-template-0.0.1.zip'
19
+
20
+ include Util
21
+
22
+ # @param [Hash] config user options
23
+ # @param [String] working_dir directory in which to do packaging work
24
+ def initialize(config = {})
25
+ merge_config config
26
+ sanitize_config
27
+ define_readers
28
+ end
29
+
30
+ def shortname
31
+ @config[:shortname] || name.downcase.gsub(/\W+/, '')
32
+ end
33
+
34
+ def validator
35
+ @validator ||= validator_class.new(self)
36
+ end
37
+
38
+ def warbler_extensions
39
+ @warbler_extensions ||= warbler_extensions_class.new(self)
40
+ end
41
+
42
+ def to_hash
43
+ @config
44
+ end
45
+
46
+ def valid?
47
+ validator.valid?
48
+ end
49
+
50
+ def validate
51
+ return unless validator.respond_to? :reset_and_validate
52
+ validator.reset_and_validate
53
+ end
54
+
55
+ def error_message_list
56
+ validator.error_message_list
57
+ end
58
+
59
+ def ==(other)
60
+ super unless other.class == self.class && other.respond_to?(:to_hash)
61
+ @config == other.to_hash && working_dir == other.working_dir
62
+ end
63
+
64
+ def to_warbler_config
65
+ warbler_config = nil
66
+ Dir.chdir working_dir do
67
+ warbler_config = Warbler::Config.new do |config|
68
+ config.jar_name = self.shortname
69
+ specs = self.gems.map do |gem|
70
+ # This is rather a hack as Gem::Specification.find_by_name(gem)
71
+ # seems to break Travis.
72
+ # See: https://github.com/shoes/shoes4/pull/989#issuecomment-68170746
73
+ Gem::Specification.find_all_by_name(gem).first
74
+ end
75
+ dependencies = specs.map { |s| s.runtime_dependencies }.flatten
76
+ (specs + dependencies).uniq.each { |g| config.gems << g }
77
+ ignore = self.ignore.map do |f|
78
+ path = f.to_s
79
+ children = Dir.glob("#{path}/**/*") if File.directory?(path)
80
+ [path, *children]
81
+ end.flatten
82
+ config.excludes.add FileList.new(ignore.flatten).pathmap(config.pathmaps.application.first)
83
+ config.gem_excludes += [/^samples/, /^examples/, /^test/, /^spec/]
84
+
85
+ warbler_extensions.customize(config) if warbler_extensions.respond_to? :customize
86
+ end
87
+ end
88
+ warbler_config
89
+ end
90
+
91
+ private
92
+ def warbler_extensions_class
93
+ @warbler_extensions_class ||= @config.fetch(:warbler_extensions)
94
+ end
95
+
96
+ def validator_class
97
+ @validator_class ||= @config.fetch(:validator)
98
+ end
99
+
100
+ # Overwrite defaults with supplied config
101
+ def merge_config(config)
102
+ defaults = {
103
+ name: 'Ruby App',
104
+ version: '0.0.0',
105
+ release: 'Rookie',
106
+ ignore: 'pkg',
107
+ # TODO: Establish these default icons and paths. These would be
108
+ # default icons for generic Ruby apps.
109
+ icons: {
110
+ #osx: 'path/to/default/App.icns',
111
+ #gtk: 'path/to/default/app.png',
112
+ #win32: 'path/to/default/App.ico',
113
+ },
114
+ template_urls: {
115
+ jar_app: JAR_APP_TEMPLATE_URL,
116
+ },
117
+ validator: Furoshiki::Validator,
118
+ warbler_extensions: Furoshiki::WarblerExtensions,
119
+ working_dir: Dir.pwd,
120
+ }
121
+
122
+ @config = merge_with_symbolized_keys(defaults, config)
123
+ end
124
+
125
+ # Ensure these keys have workable values
126
+ def sanitize_config
127
+ [:ignore, :gems].each { |k| @config[k] = Array(@config[k]) }
128
+ @config[:working_dir] = Pathname.new(@config[:working_dir])
129
+ end
130
+
131
+ # Define reader for each top-level config key (except those already defined
132
+ # explicitly)
133
+ def define_readers
134
+ metaclass = class << self; self; end
135
+ @config.keys.reject {|k| self.respond_to?(k) }.each do |k|
136
+ metaclass.send(:define_method, k) do
137
+ @config[k]
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,41 @@
1
+ require 'warbler'
2
+ require 'warbler/traits/furoshiki'
3
+
4
+ module Furoshiki
5
+ class Jar
6
+ # @param [Furoshiki::Shoes::Configuration] config user configuration
7
+ def initialize(config)
8
+ @furoshiki_config = config
9
+
10
+ unless config.valid?
11
+ raise Furoshiki::ConfigurationError, "Invalid configuration.\n#{config.error_message_list}"
12
+ end
13
+
14
+ @config = @furoshiki_config.to_warbler_config
15
+ end
16
+
17
+ def package(dir = default_dir)
18
+ Dir.chdir working_dir do
19
+ jar = Warbler::Jar.new
20
+ jar.apply @config
21
+ package_dir = dir.relative_path_from(working_dir)
22
+ package_dir.mkpath
23
+ path = package_dir.join(filename).to_s
24
+ jar.create path
25
+ File.expand_path path
26
+ end
27
+ end
28
+
29
+ def default_dir
30
+ working_dir.join 'pkg'
31
+ end
32
+
33
+ def filename
34
+ "#{@config.jar_name}.#{@config.jar_extension}"
35
+ end
36
+
37
+ def working_dir
38
+ @furoshiki_config.working_dir
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,225 @@
1
+ require 'furoshiki/exceptions'
2
+ require 'furoshiki/zip/directory'
3
+ require 'furoshiki/jar'
4
+ require 'fileutils'
5
+ require 'plist'
6
+ require 'open-uri'
7
+ require 'net/http'
8
+
9
+ module Furoshiki
10
+ class JarApp
11
+ include FileUtils
12
+
13
+ # @param [Furoshiki::Shoes::Configuration] config user configuration
14
+ def initialize(config)
15
+ @config = config
16
+
17
+ unless config.valid?
18
+ raise Furoshiki::ConfigurationError, "Invalid configuration.\n#{config.error_message_list}"
19
+ end
20
+
21
+ home = ENV['FUROSHIKI_HOME'] || Dir.home
22
+ @cache_dir = Pathname.new(home).join('.furoshiki', 'cache')
23
+ @default_package_dir = working_dir.join('pkg')
24
+ @package_dir = default_package_dir
25
+ @default_template_path = cache_dir.join(template_filename)
26
+ @template_path = default_template_path
27
+ @tmp = @package_dir.join('tmp')
28
+ end
29
+
30
+ # @return [Pathname] default package directory: ./pkg
31
+ attr_reader :default_package_dir
32
+
33
+ # @return [Pathname] package directory
34
+ attr_accessor :package_dir
35
+
36
+ # @return [Pathname] default path to .app template
37
+ attr_reader :default_template_path
38
+
39
+ # @return [Pathname] path to .app template
40
+ attr_accessor :template_path
41
+
42
+ # @return [Pathname] cache directory
43
+ attr_reader :cache_dir
44
+
45
+ attr_reader :config
46
+
47
+ attr_reader :tmp
48
+
49
+ def package
50
+ remove_tmp
51
+ create_tmp
52
+ cache_template
53
+ extract_template
54
+ inject_icon
55
+ inject_config
56
+ jar_path = ensure_jar_exists
57
+ inject_jar jar_path
58
+ move_to_package_dir tmp_app_path
59
+ tweak_permissions
60
+ rescue => e
61
+ raise e
62
+ ensure
63
+ remove_tmp
64
+ end
65
+
66
+ private
67
+ def create_tmp
68
+ tmp.mkpath
69
+ end
70
+
71
+ def remove_tmp
72
+ tmp.rmtree if tmp.exist?
73
+ end
74
+
75
+ def cache_template
76
+ cache_dir.mkpath unless cache_dir.exist?
77
+ download_template unless template_path.size?
78
+ end
79
+
80
+ def template_basename
81
+ 'shoes-app-template'
82
+ end
83
+
84
+ def template_extension
85
+ '.zip'
86
+ end
87
+
88
+ def template_filename
89
+ "#{template_basename}#{template_extension}"
90
+ end
91
+
92
+ def latest_template_version
93
+ '0.0.1'
94
+ end
95
+
96
+ def download_template
97
+ download remote_template_url, template_path
98
+ end
99
+
100
+ def download(remote_url, local_path)
101
+ download_following_redirects remote_url, local_path
102
+ end
103
+
104
+ def download_following_redirects(remote_url, local_path, redirect_limit = 5)
105
+ if redirect_limit == 0
106
+ raise Furoshiki::DownloadError,
107
+ "Too many redirects trying to reach #{remote_url}"
108
+ end
109
+
110
+ uri = URI(remote_url)
111
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
112
+ request = Net::HTTP::Get.new(uri.request_uri)
113
+ http.request request do |response|
114
+ case response
115
+ when Net::HTTPSuccess then
116
+ warn "Downloading #{remote_url} to #{local_path}"
117
+ open(local_path, 'wb') do |file|
118
+ response.read_body do |chunk|
119
+ file.write chunk
120
+ end
121
+ end
122
+ when Net::HTTPRedirection then
123
+ location = response['location']
124
+ warn "Redirected to #{location}"
125
+ download_following_redirects(location, local_path, redirect_limit - 1)
126
+ else
127
+ raise Furoshiki::DownloadError, "Couldn't download app template at #{remote_url}. #{response.value}"
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def downloads_url
134
+ "http://shoesrb.com/downloads"
135
+ end
136
+
137
+ def remote_template_url
138
+ config.template_urls.fetch(:jar_app)
139
+ end
140
+
141
+ def move_to_package_dir(path)
142
+ dest = package_dir.join(path.basename)
143
+ dest.rmtree if dest.exist?
144
+ mv path.to_s, dest
145
+ end
146
+
147
+ def ensure_jar_exists
148
+ jar = Jar.new(@config)
149
+ path = tmp.join(jar.filename)
150
+ jar.package(tmp) unless File.exist?(path)
151
+ path
152
+ end
153
+
154
+ # Injects JAR into APP. The JAR should be the only item in the
155
+ # Contents/Java directory. If this directory contains more than one
156
+ # JAR, the "first" one gets run, which may not be what we want.
157
+ #
158
+ # @param [Pathname, String] jar_path the location of the JAR to inject
159
+ def inject_jar(jar_path)
160
+ jar_dir = tmp_app_path.join('Contents/Java')
161
+ jar_dir.rmtree
162
+ jar_dir.mkdir
163
+ cp Pathname.new(jar_path), jar_dir
164
+ end
165
+
166
+ def extract_template
167
+ raise IOError, "Couldn't find app template at #{template_path}." unless template_path.size?
168
+ extracted_app = nil
169
+
170
+ ::Zip::File.open(template_path) do |zip_file|
171
+ zip_file.each do |entry|
172
+ extracted_app = template_path.join(entry.name) if Pathname.new(entry.name).extname == '.app'
173
+ p = tmp.join(entry.name)
174
+ p.dirname.mkpath
175
+ entry.extract(p)
176
+ end
177
+ end
178
+ mv tmp.join(extracted_app.basename.to_s), tmp_app_path
179
+ end
180
+
181
+ def inject_config
182
+ plist = tmp_app_path.join 'Contents/Info.plist'
183
+ template = Plist.parse_xml(plist)
184
+ template['CFBundleIdentifier'] = "com.hackety.shoes.#{config.shortname}"
185
+ template['CFBundleDisplayName'] = config.name
186
+ template['CFBundleName'] = config.name
187
+ template['CFBundleVersion'] = config.version
188
+ template['CFBundleIconFile'] = Pathname.new(config.icons[:osx]).basename.to_s if config.icons[:osx]
189
+ File.open(plist, 'w') { |f| f.write template.to_plist }
190
+ end
191
+
192
+ def inject_icon
193
+ if config.icons[:osx]
194
+ icon_path = working_dir.join(config.icons[:osx])
195
+ raise IOError, "Couldn't find app icon at #{icon_path}" unless icon_path.exist?
196
+ resources_dir = tmp_app_path.join('Contents/Resources')
197
+ cp icon_path, resources_dir.join(icon_path.basename)
198
+ end
199
+ end
200
+
201
+ def tweak_permissions
202
+ executable_path.chmod 0755
203
+ end
204
+
205
+ def app_name
206
+ "#{config.name}.app"
207
+ end
208
+
209
+ def tmp_app_path
210
+ tmp.join app_name
211
+ end
212
+
213
+ def app_path
214
+ package_dir.join app_name
215
+ end
216
+
217
+ def executable_path
218
+ app_path.join('Contents/MacOS/JavaAppLauncher')
219
+ end
220
+
221
+ def working_dir
222
+ config.working_dir
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,27 @@
1
+ module Furoshiki
2
+ module Util
3
+ # Ensure symbol keys, even in nested hashes
4
+ #
5
+ # @param [Hash] config the hash to set (key: value) on
6
+ # @param [#to_sym] k the key
7
+ # @param [Object] v the value
8
+ # @return [Hash] an updated hash
9
+ def deep_set_symbol_key(hash, key, value)
10
+ if value.kind_of? Hash
11
+ hash[key.to_sym] = value.inject({}) { |inner_hash, (inner_key, inner_value)| deep_set_symbol_key(inner_hash, inner_key, inner_value) }
12
+ else
13
+ hash[key.to_sym] = value
14
+ end
15
+ hash
16
+ end
17
+
18
+ def deep_symbolize_keys(hash)
19
+ merge_with_symbolized_keys({}, hash)
20
+ end
21
+
22
+ # Assumes that defaults already has symbolized keys
23
+ def merge_with_symbolized_keys(defaults, hash)
24
+ hash.inject(defaults) { |symbolized, (k, v)| deep_set_symbol_key(symbolized, k, v) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ module Furoshiki
2
+ class Validator
3
+ def initialize(config)
4
+ @config = config
5
+ @errors = []
6
+ end
7
+
8
+ attr_reader :config
9
+
10
+ def valid?
11
+ reset_and_validate
12
+ return errors.empty?
13
+ end
14
+
15
+ def reset_and_validate
16
+ @errors.clear
17
+ validate if respond_to? :validate
18
+ end
19
+
20
+ def errors
21
+ @errors.dup
22
+ end
23
+
24
+ def error_message_list
25
+ @errors.map {|m| " - #{m}"}.join("\n")
26
+ end
27
+
28
+ def working_dir
29
+ @config.working_dir
30
+ end
31
+
32
+ private
33
+ def add_error(message)
34
+ @errors << message
35
+ end
36
+
37
+ def add_missing_file_error(value, description)
38
+ message = "#{description} configured as '#{value}', but couldn't find file at #{working_dir.join(value.to_s)}"
39
+ add_error(message)
40
+ end
41
+ end
42
+ end
@@ -1,4 +1,4 @@
1
1
  module Furoshiki
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
4
4
 
@@ -0,0 +1,12 @@
1
+ module Furoshiki
2
+ class WarblerExtensions
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ # Override to customize config
8
+ def customize(warbler_config)
9
+ warbler_config
10
+ end
11
+ end
12
+ end
data/lib/furoshiki.rb CHANGED
@@ -1,3 +1,4 @@
1
- puts "OMG, nothing here!"
2
- puts ""
3
- puts "You probably want to `require 'furoshiki/shoes'`"
1
+ require 'furoshiki/configuration'
2
+ require 'furoshiki/exceptions'
3
+ require 'furoshiki/jar'
4
+ require 'furoshiki/jar_app'
@@ -0,0 +1,31 @@
1
+ require 'warbler'
2
+ require 'warbler/traits'
3
+
4
+ module Warbler
5
+ module Traits
6
+ # Hack to stop bundler injecting itself
7
+ class Bundler
8
+ def self.detect?
9
+ false
10
+ end
11
+ end
12
+
13
+ class Furoshiki
14
+ include ::Warbler::Trait
15
+ include PathmapHelper
16
+
17
+ def self.detect?
18
+ true
19
+ end
20
+
21
+ def self.requires?(trait)
22
+ [Traits::Jar].include? trait
23
+ end
24
+
25
+ def update_archive(jar)
26
+ # Not sure why Warbler doesn't do this automatically
27
+ jar.files.delete_if { |k, v| @config.excludes.include? k }
28
+ end
29
+ end
30
+ end
31
+ end