Permalink
Browse files

Add packagers

  • Loading branch information...
1 parent b16f3fb commit 589cca7fc41cccd4baa2b7924d990bbdd6b222f6 @wasnotrice wasnotrice committed Oct 19, 2012
View
60 app.yaml
@@ -0,0 +1,60 @@
+# app.yaml
+
+# -----------------------------------------------------------------------------
+# This is a sample configuration file for a Shoes app. Edit at will.
+# -----------------------------------------------------------------------------
+
+# The app's full name, intended for humans. This will be the name of the thing
+# you double-click.
+name: Shoes App
+
+# A short version of the name, simplified for under-the-hood use. Defaults to
+# lowercased name, with non-word characters removed.
+shortname: shoesapp
+
+# Files to ignore when bundling the app. Should be an array.
+ignore:
+ - 'vendor'
+ - 'lib'
+ - 'static'
+ - '*.app'
+ - 'spec'
+
+# How to run the app
+run: bin/hello_from_warbler
+
+# -----------------------------------------------------------------------------
+# The following options are new in Shoes 4. They don't work in Shoes 3.
+# -----------------------------------------------------------------------------
+
+# Installed gems to bundle with the app
+gems:
+
+# -----------------------------------------------------------------------------
+# The following options are only available for Shoes 3. They won't work (yet)
+# in Shoes 4.
+# -----------------------------------------------------------------------------
+
+# The app's version number
+version: 0.0.1
+
+# The name of this release
+release: Mindfully
+
+# App icons for various platforms
+icons:
+ osx: images/Shoes.icns
+ gtk: path/to/app.png
+ win32: path/to/App.ico
+
+# Artwork for a DMG
+dmg:
+ ds_store: path/to/.DS_Store
+ background: path/to/background.png
+
+# The command to clone the app using git. Note that the prefix included here is
+# an in-package prefix, so unless you know what the inside of your package
+# looks like, this is not a recommended option. This sample is for a Shoes 3 app
+# and WILL NOT WORK on Shoes 4
+#clone: git checkout-index --prefix=dist/app/ -a
+
View
BIN images/Shoes.icns
Binary file not shown.
View
26 lib/shoes/package.rb
@@ -0,0 +1,26 @@
+module Shoes
+ module Package
+ class P
+ def initialize(backend, wrapper)
+ @backend, @wrapper = backend, wrapper
+ end
+
+ def package
+ puts "Packaging as #{@backend}:#{@wrapper}"
+ end
+ end
+ def self.new(backend, wrapper, config)
+ # Belongs in Shoes::Configuration
+ require "shoes/#{backend.to_s.downcase}"
+ backend_class_name = backend.to_s.capitalize
+ wrapper_class_name = wrapper.to_s.capitalize
+ klass = [backend_class_name, 'Package', wrapper_class_name].inject(Shoes) do |klass, const|
+ klass.const_get(const)
+ end
+ klass.new config
+ rescue LoadError => e
+ raise LoadError, "Couldn't load backend '#{backend}'. Error: #{e.message}\n#{e.backtrace.join("\n")}"
+ P.new(backend, wrapper)
+ end
+ end
+end
View
143 lib/shoes/package/configuration.rb
@@ -0,0 +1,143 @@
+require 'pathname'
+require 'yaml'
+
+module Shoes
+ module Package
+ # Configuration for Shoes packagers.
+ #
+ # @example
+ # config_file = '/path/to/app.yaml'
+ # config = Shoes::Package::Configuration.load(config_file)
+ #
+ # If your configuration uses hashes, the keys will always be
+ # symbols, even if you have created it with string keys. It's just
+ # easier that way.
+ #
+ # This is a value object. If you need to modify your configuration
+ # after initialization, dump it with #to_hash, make your changes,
+ # and instantiate a new object.
+ class Configuration
+ # Convenience method for loading config from a file. Note that you
+ # can pass four kinds of paths to the loader. Given the following
+ # file structure:
+ #
+ # ├── a
+ # │   ├── app.yaml
+ # │   └── shoes-app-a.rb
+ # └── b
+ # └── shoes-app-b.rb
+ #
+ # To package an app that has an `app.yaml`, like `shoes-app-a.rb`,
+ # you can call the loader with any of:
+ #
+ # - a/app.yaml
+ # - a
+ # - a/shoes-app-a.rb
+ #
+ # These will all find and use your configuration in `a/app.yaml`.
+ # To package an app that does not have an `app.yaml`, like
+ # `b/shoes-app-b.rb`, you must call the loader with the path of
+ # the script itself. Note that without an `app.yaml`, you will
+ # only bundle a single file, and your app will simply use the
+ # Shoes app icon.
+ #
+ # @overload load(path)
+ # @param [String] path location of the app's 'app.yaml'
+ # @overload load(path)
+ # @param [String] path location of the directory that
+ # contains the app's 'app.yaml'
+ # @overload load(path)
+ # @param [String] path location of the app
+ def self.load(path = 'app.yaml')
+ pathname = Pathname.new(path)
+ app_yaml = Pathname.new('app.yaml')
+
+ dummy_file = Struct.new(:read)
+
+ if pathname.basename == app_yaml
+ file, dir = pathname, pathname.dirname
+ elsif pathname.directory?
+ file, dir = pathname.join(app_yaml), pathname
+ elsif pathname.file? && pathname.parent.children.include?(pathname.parent.join app_yaml)
+ file, dir = pathname.parent.join(app_yaml), pathname.parent
+ else
+ # Can't find any 'app.yaml', so assume we just want to wrap
+ # this file. If it exists, load default options. If not, let
+ # the filesystem raise an error.
+ default_options = {run: pathname.basename.to_s}.to_yaml
+ options = pathname.exist? ? default_options : pathname
+ file = dummy_file.new(options)
+ dir = pathname.parent
+ end
+ new YAML.load(file.read), dir
+ end
+
+ # @param [Hash] config user options
+ # @param [String] working_dir directory in which do packaging work
+ def initialize(config = {}, working_dir = Dir.pwd)
+ defaults = {
+ name: 'Shoes App',
+ version: '0.0.0',
+ release: 'Rookie',
+ run: nil,
+ ignore: 'pkg',
+ icons: {
+ #osx: 'path/to/default/App.icns',
+ #gtk: 'path/to/default/app.png',
+ #win32: 'path/to/default/App.ico',
+ },
+ dmg: {
+ ds_store: 'path/to/default/.DS_Store',
+ background: 'path/to/default/background.png'
+ }
+ }
+
+ # Overwrite defaults with supplied config
+ @config = config.inject(defaults) { |c, (k, v)| set_symbol_key c, k, v }
+
+ # Ensure that we always have what we need
+ @config[:shortname] ||= @config[:name].downcase.gsub(/\W+/, '')
+ [:ignore, :gems].each { |k| @config[k] = Array(@config[k]) }
+ @config[:gems] << 'shoes'
+
+ # Define reader for each key
+ metaclass = class << self; self; end
+ @config.keys.each do |k|
+ metaclass.send(:define_method, k) do
+ @config[k]
+ end
+ end
+
+ @working_dir = Pathname.new(working_dir)
+ end
+
+ # @return [Pathname] the current working directory
+ attr_reader :working_dir
+
+ def to_hash
+ @config
+ end
+
+ def ==(other)
+ super unless other.class == self.class && other.respond_to?(:to_hash)
+ @config == other.to_hash
+ end
+
+ private
+ # Ensure symbol keys, even in nested hashes
+ #
+ # @param [Hash] config the hash to set (key: value) on
+ # @param [#to_sym] k the key
+ # @param [Object] v the value
+ # @return [Hash] an updated hash
+ def set_symbol_key(config, k, v)
+ if v.kind_of? Hash
+ config[k.to_sym] = v.inject({}) { |hash, (k, v)| set_symbol_key(hash, k, v) }
+ else
+ config[k.to_sym] = v
+ end
+ config
+ end
+ end
+ end
+end
View
58 lib/shoes/package/recursive_zip.rb
@@ -0,0 +1,58 @@
+require 'pathname'
+require 'zip/zip'
+
+module Shoes
+ module Package
+ # Adapted from rubyzip's sample, ZipFileGenerator
+ #
+ # This is a utility class that uses rubyzip to recursively
+ # generate a zip file containing the given entries and all of
+ # their children.
+ #
+ # Best used through frontend classes ZipDirectory or
+ # ZipDirectoryContents
+ #
+ # @example
+ # To zip the directory "/tmp/input" so that unarchiving
+ # gives you a single directory "input":
+ #
+ # zip = RecursiveZip
+ # entries = Pathname.new("/tmp/input").entries
+ # zip_prefix = ''
+ # disk_prefix = '/tmp'
+ # output_file = '/tmp/out.zip'
+ # zf.write(entries, disk_prefix, zip_prefix, output_file)
+ class RecursiveZip
+ def initialize(output_file)
+ @output_file = output_file.to_s
+ end
+
+ # @param [Array<Pathname>] entries the initial set of files to include
+ # @param [Pathname] disk_prefix a path prefix for existing entries
+ # @param [Pathname] zip_prefix a path prefix to add within archive
+ # @param [Pathname] output_file the location of the output archive
+ def write(entries, disk_prefix, zip_prefix)
+ io = Zip::ZipFile.open(@output_file, Zip::ZipFile::CREATE);
+ write_entries(entries, disk_prefix, zip_prefix, io)
+ io.close();
+ end
+
+ # A helper method to make the recursion work.
+ private
+ def write_entries(entries, disk_prefix, path, io)
+ entries.each do |e|
+ zip_path = path.to_s == "" ? e.basename : path.join(e.basename)
+ disk_path = disk_prefix.join(zip_path)
+ puts "Deflating #{disk_path}"
+ if disk_path.directory?
+ io.mkdir(zip_path)
+ subdir = disk_path.children(false)
+ write_entries(subdir, disk_prefix, zip_path, io)
+ else
+ io.get_output_stream(zip_path) { |f| f.puts(File.open(disk_path, "rb").read())}
+ end
+ end
+ end
+ end
+ end
+end
View
19 lib/shoes/package/zip_directory.rb
@@ -0,0 +1,19 @@
+require 'shoes/package/recursive_zip'
+
+module Shoes
+ module Package
+ class ZipDirectory
+ # @param [#to_s] input_dir the directory to zip
+ # @param [#to_s] output_file the location of the output archive
+ def initialize(input_dir, output_file)
+ @input_dir = Pathname.new(input_dir)
+ @zip = RecursiveZip.new(output_file)
+ end
+
+ # Zip the whole input directory, including the root
+ def write
+ @zip.write [@input_dir.basename], @input_dir.parent, ''
+ end
+ end
+ end
+end
View
20 lib/shoes/package/zip_directory_contents.rb
@@ -0,0 +1,20 @@
+require 'shoes/package/recursive_zip'
+
+module Shoes
+ module Package
+ class ZipDirectoryContents
+ # @param [#to_s] input_dir the directory to zip
+ # @param [#to_s] output_file the location of the output archive
+ def initialize(input_dir, output_file)
+ @input_dir = Pathname.new(input_dir)
+ @zip = RecursiveZip.new(output_file)
+ end
+
+ # Zip the contents of the input directory, without the root.
+ def write
+ entries = @input_dir.children(false)
+ @zip.write entries, @input_dir, ''
+ end
+ end
+ end
+end
View
148 lib/shoes/swt/package/app.rb
@@ -0,0 +1,148 @@
+require 'shoes/package/configuration'
+require 'shoes/package/zip_directory'
+require 'shoes/swt/package/jar'
+require 'fileutils'
+require 'plist'
+
+module Shoes
+ module Swt
+ module Package
+ class App
+ include FileUtils
+
+ # @param [Shoes::Package::Configuration] config user configuration
+ def initialize(config)
+ @config = config
+ @default_package_dir = working_dir.join('pkg')
+ @package_dir = default_package_dir
+ root = Pathname.new(__FILE__).join('../../../../..')
+ @default_template_path = root.join('static/shoes-app-template.zip')
+ @template_path = default_template_path
+ @tmp = @package_dir.join('tmp')
+ end
+
+ # @return [Pathname] default package directory: ./pkg
+ attr_reader :default_package_dir
+
+ # @return [Pathname] package directory
+ attr_accessor :package_dir
+
+ # @return [Pathname] default path to .app template
+ attr_reader :default_template_path
+
+ # @return [Pathname] path to .app template
+ attr_accessor :template_path
+
+ attr_reader :config
+
+ attr_reader :tmp
+
+ def package
+ remove_tmp
+ create_tmp
+ extract_template
+ inject_icon
+ inject_config
+ jar_path = ensure_jar_exists
+ inject_jar jar_path
+ move_to_package_dir tmp_app_path
+ tweak_permissions
+ rescue => e
+ raise e
+ ensure
+ remove_tmp
+ end
+
+ def create_tmp
+ tmp.mkpath
+ end
+
+ def remove_tmp
+ tmp.rmtree if tmp.exist?
+ end
+
+ def move_to_package_dir(path)
+ dest = package_dir.join(path.basename)
+ dest.rmtree if dest.exist?
+ mv path.to_s, dest
+ end
+
+ def ensure_jar_exists
+ jar = Jar.new(@config)
+ path = tmp.join(jar.filename)
+ jar.package(tmp) unless File.exist?(path)
+ path
+ end
+
+ # Injects JAR into APP. The JAR should be the only item in the
+ # Contents/Java directory. If this directory contains more than one
+ # JAR, the "first" one gets run, which may not be what we want.
+ #
+ # @param [Pathname, String] jar_path the location of the JAR to inject
+ def inject_jar(jar_path)
+ jar_dir = tmp_app_path.join('Contents/Java')
+ jar_dir.rmtree
+ jar_dir.mkdir
+ cp Pathname.new(jar_path), jar_dir
+ end
+
+ def extract_template
+ raise IOError, "Couldn't find app template at #{template_path}." unless template_path.exist?
+ extracted_app = nil
+ Zip::ZipFile.new(template_path).each do |entry|
+ # Fragile hack
+ extracted_app = template_path.join(entry.name) if Pathname.new(entry.name).extname == '.app'
+ p = tmp.join(entry.name)
+ p.dirname.mkpath
+ entry.extract(p)
+ end
+ mv tmp.join(extracted_app.basename.to_s), tmp_app_path
+ end
+
+ def inject_config
+ plist = tmp_app_path.join 'Contents/Info.plist'
+ template = Plist.parse_xml(plist)
+ template['CFBundleIdentifier'] = "com.hackety.shoes.#{config.shortname}"
+ template['CFBundleDisplayName'] = config.name
+ template['CFBundleName'] = config.name
+ template['CFBundleVersion'] = config.version
+ template['CFBundleIconFile'] = Pathname.new(config.icons[:osx]).basename.to_s if config.icons[:osx]
+ File.open(plist, 'w') { |f| f.write template.to_plist }
+ end
+
+ def inject_icon
+ if config.icons[:osx]
+ icon_path = working_dir.join(config.icons[:osx])
+ raise IOError, "Couldn't find app icon at #{icon_path}" unless icon_path.exist?
+ resources_dir = tmp_app_path.join('Contents/Resources')
+ cp icon_path, resources_dir.join(icon_path.basename)
+ end
+ end
+
+ def tweak_permissions
+ executable_path.chmod 0755
+ end
+
+ def app_name
+ "#{config.name}.app"
+ end
+
+ def tmp_app_path
+ tmp.join app_name
+ end
+
+ def app_path
+ package_dir.join app_name
+ end
+
+ def executable_path
+ app_path.join('Contents/MacOS/JavaAppLauncher')
+ end
+
+ def working_dir
+ config.working_dir
+ end
+ end
+ end
+ end
+end
View
60 lib/shoes/swt/package/jar.rb
@@ -0,0 +1,60 @@
+require 'warbler'
+require 'warbler/traits/shoes'
+
+module Shoes
+ module Swt
+ module Package
+ class Jar
+ # @param [Shoes::Package::Configuration] config user configuration
+ def initialize(config = nil)
+ @shoes_config = config || ::Shoes::Package::Configuration.load
+ Dir.chdir working_dir do
+ @config = Warbler::Config.new do |config|
+ config.jar_name = @shoes_config.shortname
+ config.pathmaps.application = ['shoes-app/%p']
+ specs = @shoes_config.gems.map { |g| Gem::Specification.find_by_name(g) }
+ dependencies = specs.map { |s| s.runtime_dependencies }.flatten
+ (specs + dependencies).uniq.each { |g| config.gems << g }
+ ignore = @shoes_config.ignore.map do |f|
+ path = f.to_s
+ children = Dir.glob("#{path}/**/*") if File.directory?(path)
+ [path, *children]
+ end.flatten
+ config.excludes.add FileList.new(ignore).pathmap(config.pathmaps.application.first)
+ end
+ @config.extend ShoesWarblerConfig
+ @config.run = @shoes_config.run.split(/\s/).first
+ end
+ end
+
+ def package(dir = default_dir)
+ Dir.chdir working_dir do
+ jar = Warbler::Jar.new
+ jar.apply @config
+ path = dir.relative_path_from(working_dir).join(filename).to_s
+ jar.create path
+ path
+ end
+ end
+
+ def default_dir
+ 'pkg'
+ end
+
+ def filename
+ "#{@config.jar_name}.#{@config.jar_extension}"
+ end
+
+ def working_dir
+ @shoes_config.working_dir
+ end
+
+ private
+ # Adds Shoes-specific functionality to the Warbler Config
+ module ShoesWarblerConfig
+ attr_accessor :run
+ end
+ end
+ end
+ end
+end
View
51 lib/warbler/traits/shoes.rb
@@ -0,0 +1,51 @@
+require 'shoes/package/configuration'
+
+module Warbler
+ module Traits
+ # Hack to control the executable
+ class NoGemspec
+ def update_archive(jar); end
+ end
+
+ class Shoes
+ include Trait
+ include PathmapHelper
+
+ def self.detect?
+ #File.exist? "app.yaml"
+ true
+ end
+
+ def self.requires?(trait)
+ # Actually, it would be better to dump the NoGemspec trait, but since
+ # we can't do that, we can at least make sure that this trait gets
+ # processed later by declaring that it requires NoGemspec.
+ [Traits::Jar, Traits::NoGemspec].include? trait
+ end
+
+ def after_configure
+ config.init_contents << StringIO.new("require 'shoes'\nShoes.configuration.backend = :swt\n")
+ end
+
+ def update_archive(jar)
+ # Not sure why Warbler doesn't do this automatically
+ jar.files.delete_if { |k, v| @config.excludes.include? k }
+ add_main_rb(jar, apply_pathmaps(config, default_executable, :application))
+ end
+
+ # Uses the `@config.run` if it exists. Otherwise, looks in the
+ # application's `bin` directory for an executable with the same name as
+ # the jar. If this also fails, defaults to the first executable (alphabetically) in the
+ # applications `bin` directory.
+ #
+ # @return [String] filename of the executable to run
+ def default_executable
+ return @config.run if @config.run
+ exes = Dir['bin/*'].sort
+ exe = exes.grep(/#{config.jar_name}/).first || exes.first
+ raise "No executable script found" unless exe
+ exe
+ end
+ end
+ end
+end
View
148 spec/shoes/package_configuration_spec.rb
@@ -0,0 +1,148 @@
+require 'spec_helper'
+require 'shoes/package/configuration'
+
+describe Shoes::Package::Configuration do
+ context "defaults" do
+ subject { Shoes::Package::Configuration.new }
+
+ its(:name) { should eq('Shoes App') }
+ its(:shortname) { should eq('shoesapp') }
+ its(:ignore) { should eq(['pkg']) }
+ its(:gems) { should include('shoes') }
+ its(:version) { should eq('0.0.0') }
+ its(:release) { should eq('Rookie') }
+ its(:icons) { should be_an_instance_of(Hash) }
+ its(:dmg) { should be_an_instance_of(Hash) }
+ its(:run) { should be_nil }
+
+ describe "#icon" do
+ it 'osx is nil' do
+ subject.icons[:osx].should be_nil
+ end
+
+ it 'gtk is nil' do
+ subject.icons[:gtk].should be_nil
+ end
+
+ it 'win32 is nil' do
+ subject.icons[:win32].should be_nil
+ end
+ end
+
+ describe "#dmg" do
+ it "has ds_store" do
+ subject.dmg[:ds_store].should eq('path/to/default/.DS_Store')
+ end
+
+ it "has background" do
+ subject.dmg[:background].should eq('path/to/default/background.png')
+ end
+ end
+
+ describe "#to_hash" do
+ it "round-trips" do
+ Shoes::Package::Configuration.new(subject.to_hash).should eq(subject)
+ end
+ end
+ end
+
+ context "with options" do
+ include_context 'config'
+ subject { Shoes::Package::Configuration.load(config_filename) }
+
+ its(:name) { should eq('Sugar Clouds') }
+ its(:shortname) { should eq('sweet-nebulae') }
+ its(:ignore) { should include('pkg') }
+ its(:gems) { should include('rspec') }
+ its(:gems) { should include('shoes') }
+ its(:version) { should eq('0.0.1') }
+ its(:release) { should eq('Mindfully') }
+ its(:icons) { should be_an_instance_of(Hash) }
+ its(:dmg) { should be_an_instance_of(Hash) }
+
+ describe "#icon" do
+ it 'has osx' do
+ subject.icons[:osx].should eq('img/boots.icns')
+ end
+
+ it 'has gtk' do
+ subject.icons[:gtk].should eq('img/boots_512x512x32.png')
+ end
+
+ it 'has win32' do
+ subject.icons[:win32].should eq('img/boots.ico')
+ end
+ end
+
+ describe "#dmg" do
+ it "has ds_store" do
+ subject.dmg[:ds_store].should eq('path/to/custom/.DS_Store')
+ end
+
+ it "has background" do
+ subject.dmg[:background].should eq('path/to/custom/background.png')
+ end
+ end
+
+ it "incorporates custom features" do
+ subject.custom.should eq('my custom feature')
+ end
+ end
+
+ context "with name, but without explicit shortname" do
+ let(:options) { {:name => "Sugar Clouds"} }
+ subject { Shoes::Package::Configuration.new options }
+
+ its(:name) { should eq("Sugar Clouds") }
+ its(:shortname) { should eq("sugarclouds") }
+ end
+
+ context "auto-loading" do
+ include_context 'config'
+
+ context "without a path" do
+ it "looks for 'app.yaml' in current directory" do
+ Dir.chdir config_filename.parent do
+ config = Shoes::Package::Configuration.load
+ config.shortname.should eq('sweet-nebulae')
+ end
+ end
+
+ it "blows up if it can't find the file" do
+ Dir.chdir File.dirname(__FILE__) do
+ lambda { config = Shoes::Package::Configuration.load }.should raise_error
+ end
+ end
+ end
+
+ shared_examples "config with path" do
+ it "finds the config" do
+ Dir.chdir File.dirname(__FILE__) do
+ config = Shoes::Package::Configuration.load(path)
+ config.shortname.should eq('sweet-nebulae')
+ end
+ end
+ end
+
+ context "with an 'app.yaml'" do
+ let(:path) { config_filename }
+ it_behaves_like "config with path"
+ end
+
+ context "with a path to a directory containing an 'app.yaml'" do
+ let(:path) { config_filename.parent }
+ it_behaves_like "config with path"
+ end
+
+ context "with a path to a file that is siblings with an 'app.yaml'" do
+ let(:path) { config_filename.parent.join('sibling.rb') }
+ it_behaves_like "config with path"
+ end
+
+ context "when the file doesn't exist" do
+ it "blows up" do
+ lambda { Shoes::Package::Configuration.load('some/bogus/path') }.should raise_error
+ end
+ end
+ end
+end
View
26 spec/shoes/zip_directory_contents_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'support/shared_zip'
+require 'fileutils'
+require 'shoes/package/zip_directory_contents'
+
+describe Shoes::Package::ZipDirectoryContents do
+ subject { Shoes::Package::ZipDirectoryContents.new input_dir, output_file }
+
+ context "output file" do
+ include_context 'zip'
+
+ it "exists" do
+ output_file.should exist
+ end
+
+ it "does not include input directory without parents" do
+ zip.entries.map(&:name).should_not include(add_trailing_slash input_dir.basename)
+ end
+
+ relative_input_paths(input_dir).each do |path|
+ it "includes all children of input directory" do
+ zip.entries.map(&:name).should include(path)
+ end
+ end
+ end
+end
View
31 spec/shoes/zip_directory_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'support/shared_zip'
+require 'fileutils'
+require 'shoes/package/zip_directory'
+
+describe Shoes::Package::ZipDirectory do
+ subject { Shoes::Package::ZipDirectory.new input_dir, output_file }
+
+ context "output file" do
+ include_context 'zip'
+
+ it "exists" do
+ output_file.should exist
+ end
+
+ it "includes input directory without parents" do
+ zip.entries.map(&:name).should include(add_trailing_slash input_dir.basename)
+ end
+
+ relative_input_paths(input_dir.parent).each do |path|
+ it "includes all children of input directory" do
+ zip.entries.map(&:name).should include(path)
+ end
+ end
+
+ it "doesn't include extra files" do
+ number_of_files = Dir.glob("#{input_dir}/**/*").push(input_dir).length
+ zip.entries.length.should eq(number_of_files)
+ end
+ end
+end
View
33 spec/spec_helper.rb
@@ -2,11 +2,38 @@
$LOAD_PATH << File.join('../lib', SHOESSPEC_ROOT)
require 'rspec'
-
+require 'pathname'
require 'pry'
require 'shoes'
-Dir["#{SHOESSPEC_ROOT}/support/**/*.rb"].each {|f| require f}
+module PackageHelpers
+ # need these values from a context block, so let doesn't work
+ def spec_dir
+ Pathname.new(__FILE__).parent
+ end
+
+ def input_dir
+ spec_dir.join 'support', 'zip'
+ end
+end
+
+module ZipHelpers
+ include PackageHelpers
+ # dir = Pathname.new('spec/support/zip')
+ # add_trailing_slash(dir) #=> '/path/to/spec/support/zip/'
+ def add_trailing_slash(dir)
+ dir.to_s + "/"
+ end
+
+ def relative_input_paths(from_dir)
+ Pathname.glob(input_dir + "**/*").map do |p|
+ directory = true if p.directory?
+ relative_path = p.relative_path_from(from_dir).to_s
+ relative_path = add_trailing_slash(relative_path) if directory
+ relative_path
+ end
+ end
+end
# Guards for running or not running specs. Specs in the guarded block only
# run if the guard conditions are met.
@@ -28,3 +55,5 @@ def backend_is(backend)
include Guard
+Dir["#{SHOESSPEC_ROOT}/support/**/*.rb"].each {|f| require f}
+
View
6 spec/support/shared_config.rb
@@ -0,0 +1,6 @@
+require 'yaml'
+require 'pathname'
+
+shared_context 'config' do
+ let(:config_filename) { Pathname.new(__FILE__).join('../../test_app/app.yaml').cleanpath }
+end
View
21 spec/support/shared_zip.rb
@@ -0,0 +1,21 @@
+include ZipHelpers
+
+shared_context 'package' do
+ let(:app_dir) { spec_dir.join 'test_app' }
+ let(:output_dir) { app_dir.join 'pkg' }
+end
+
+shared_context 'zip' do
+ include_context 'package'
+ let(:output_file) { output_dir.join 'zip_directory_spec.zip' }
+ let(:zip) { Zip::ZipFile.open output_file }
+
+ before :all do
+ output_dir.mkpath
+ subject.write
+ end
+
+ after :all do
+ FileUtils.rm_rf output_dir
+ end
+end
View
1 spec/support/zip/a/a.rb
@@ -0,0 +1 @@
+puts 'a'
View
BIN spec/support/zip/a/b/b.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
1 spec/support/zip/a/b/c/c.rb
@@ -0,0 +1 @@
+puts 'c'
View
95 spec/swt_shoes/package_app_spec.rb
@@ -0,0 +1,95 @@
+require 'spec_helper'
+require 'pathname'
+require 'shoes/swt/package/app'
+
+include PackageHelpers
+
+describe Shoes::Swt::Package::App do
+ include_context 'config'
+ include_context 'package'
+
+ let(:app_name) { 'Sugar Clouds.app' }
+ let(:output_file) { output_dir.join app_name }
+ let(:config) { Shoes::Package::Configuration.load config_filename}
+ let(:launcher) { output_file.join('Contents/MacOS/JavaAppLauncher') }
+ let(:icon) { output_file.join('Contents/Resources/boots.icns') }
+ let(:jar) { output_file.join('Contents/Java/sweet-nebulae.jar') }
+ subject { Shoes::Swt::Package::App.new config }
+
+ context "default" do
+ it "package dir is {pwd}/pkg" do
+ Dir.chdir app_dir do
+ subject.default_package_dir.should eq(app_dir.join 'pkg')
+ end
+ end
+
+ its(:template_path) { should eq(spec_dir.parent.join('static/shoes-app-template.zip')) }
+ its(:template_path) { should exist }
+ end
+
+ context "when creating a .app" do
+ before :all do
+ output_dir.rmtree if output_dir.exist?
+ output_dir.mkpath
+ Dir.chdir app_dir do
+ subject.package
+ end
+ end
+
+ it "creates a .app" do
+ output_file.should exist
+ end
+
+ it "includes launcher" do
+ launcher.should exist
+ end
+
+ it "makes launcher executable" do
+ launcher.should be_executable
+ end
+
+ it "deletes generic icon" do
+ icon.parent.join('GenericApp.icns').should_not exist
+ end
+
+ it "injects icon" do
+ icon.should exist
+ end
+
+ it "injects jar" do
+ jar.should exist
+ end
+
+ it "removes any extraneous jars" do
+ jar_dir_contents = output_file.join("Contents/Java").children
+ jar_dir_contents.reject {|f| f == jar }.should be_empty
+ end
+
+ describe "Info.plist" do
+ require 'plist'
+ before :all do
+ @plist = Plist.parse_xml(output_file.join 'Contents/Info.plist')
+ end
+
+ it "sets identifier" do
+ @plist['CFBundleIdentifier'].should eq('com.hackety.shoes.sweet-nebulae')
+ end
+
+ it "sets display name" do
+ @plist['CFBundleDisplayName'].should eq('Sugar Clouds')
+ end
+
+ it "sets bundle name" do
+ @plist['CFBundleName'].should eq('Sugar Clouds')
+ end
+
+ it "sets icon" do
+ @plist['CFBundleIconFile'].should eq('boots.icns')
+ end
+
+ it "sets version" do
+ @plist['CFBundleVersion'].should eq('0.0.1')
+ end
+ end
+ end
+end
View
43 spec/swt_shoes/package_jar_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require 'pathname'
+require 'shoes/swt/package/jar'
+
+include PackageHelpers
+
+describe Shoes::Swt::Package::Jar do
+ include_context 'config'
+ include_context 'package'
+
+ context "when creating a .jar" do
+ before :all do
+ output_dir.rmtree if output_dir.exist?
+ output_dir.mkpath
+ Dir.chdir app_dir do
+ @jar_path = subject.package(output_dir)
+ end
+ end
+
+ let(:jar_name) { 'sweet-nebulae.jar' }
+ let(:output_file) { Pathname.new(output_dir.join jar_name) }
+
+ it "creates a .jar" do
+ output_file.should exist
+ end
+
+ it "returns path to .jar" do
+ @jar_path.should eq(output_file.to_s)
+ end
+
+ it "creates .jar smaller than 40MB" do
+ File.size(output_file).should be < 40 * 1024 * 1024
+ end
+
+ it "excludes directories recursively" do
+ jar = Zip::ZipFile.new(output_file)
+ jar.entries.should_not include("dir_to_ignore/file_to_ignore")
+ end
+
+ its(:default_dir) { should eq('pkg') }
+ its(:filename) { should eq(jar_name) }
+ end
+end
View
17 spec/test_app/app.yaml
@@ -0,0 +1,17 @@
+name: Sugar Clouds
+shortname: sweet-nebulae
+ignore:
+ - pkg
+ - dir_to_ignore
+run: bin/hello_world
+gems: rspec
+version: 0.0.1
+release: Mindfully
+icons:
+ osx: img/boots.icns
+ gtk: img/boots_512x512x32.png
+ win32: img/boots.ico
+dmg:
+ ds_store: path/to/custom/.DS_Store
+ background: path/to/custom/background.png
+custom: my custom feature
View
3 spec/test_app/bin/hello_world
@@ -0,0 +1,3 @@
+Shoes.app do
+ banner "Hello world"
+end
View
1 spec/test_app/dir_to_ignore/file_to_ignore.txt
@@ -0,0 +1 @@
+should be ignored
View
BIN spec/test_app/img/boots.icns
Binary file not shown.
View
BIN spec/test_app/img/boots.ico
Binary file not shown.
View
BIN spec/test_app/img/boots_512x512x32.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
1 spec/test_app/sibling.rb
@@ -0,0 +1 @@
+# Just a sibling of 'app.yaml'

0 comments on commit 589cca7

Please sign in to comment.