releasy 0.2.0rc1

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 (74) hide show
  1. data/.gitignore +17 -0
  2. data/.yardopts +4 -0
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +165 -0
  7. data/Rakefile +31 -0
  8. data/bin/7z.sfx +0 -0
  9. data/bin/releasy +7 -0
  10. data/lib/releasy.rb +18 -0
  11. data/lib/releasy/archivers.rb +12 -0
  12. data/lib/releasy/archivers/archiver.rb +74 -0
  13. data/lib/releasy/archivers/dmg.rb +18 -0
  14. data/lib/releasy/archivers/exe.rb +23 -0
  15. data/lib/releasy/archivers/seven_zip.rb +12 -0
  16. data/lib/releasy/archivers/tar_archiver.rb +14 -0
  17. data/lib/releasy/archivers/tar_bzip2.rb +13 -0
  18. data/lib/releasy/archivers/tar_gzip.rb +13 -0
  19. data/lib/releasy/archivers/zip.rb +12 -0
  20. data/lib/releasy/builders.rb +12 -0
  21. data/lib/releasy/builders/builder.rb +52 -0
  22. data/lib/releasy/builders/ocra_builder.rb +43 -0
  23. data/lib/releasy/builders/osx_app.rb +158 -0
  24. data/lib/releasy/builders/source.rb +27 -0
  25. data/lib/releasy/builders/windows_builder.rb +65 -0
  26. data/lib/releasy/builders/windows_folder.rb +47 -0
  27. data/lib/releasy/builders/windows_folder_from_ruby_dist.rb +150 -0
  28. data/lib/releasy/builders/windows_installer.rb +117 -0
  29. data/lib/releasy/builders/windows_standalone.rb +37 -0
  30. data/lib/releasy/cli.rb +12 -0
  31. data/lib/releasy/cli/install_sfx.rb +66 -0
  32. data/lib/releasy/dsl_wrapper.rb +71 -0
  33. data/lib/releasy/mixins/exec.rb +14 -0
  34. data/lib/releasy/mixins/has_archivers.rb +37 -0
  35. data/lib/releasy/mixins/has_gemspecs.rb +42 -0
  36. data/lib/releasy/mixins/register.rb +47 -0
  37. data/lib/releasy/project.rb +300 -0
  38. data/lib/releasy/version.rb +3 -0
  39. data/lib/releasy/windows_wrapper_maker.rb +90 -0
  40. data/lib/releasy/windows_wrapper_maker/Gemfile +8 -0
  41. data/media/releasy.png +0 -0
  42. data/releasy.gemspec +37 -0
  43. data/test/releasy/archivers_test.rb +65 -0
  44. data/test/releasy/builders/data/Info.plist +51 -0
  45. data/test/releasy/builders/data/Main.rb +12 -0
  46. data/test/releasy/builders/data/relapse_runner.rb +1 -0
  47. data/test/releasy/builders/data/set_app_executable.sh +3 -0
  48. data/test/releasy/builders/helpers/helper.rb +47 -0
  49. data/test/releasy/builders/ocra_builder_test.rb +37 -0
  50. data/test/releasy/builders/osx_app_test.rb +107 -0
  51. data/test/releasy/builders/source_test.rb +43 -0
  52. data/test/releasy/builders/windows_builder_test.rb +26 -0
  53. data/test/releasy/builders/windows_folder_from_ruby_dist_test.rb +105 -0
  54. data/test/releasy/builders/windows_folder_test.rb +56 -0
  55. data/test/releasy/builders/windows_installer_test.rb +62 -0
  56. data/test/releasy/builders/windows_standalone_test.rb +58 -0
  57. data/test/releasy/cli/install_sfx_test.rb +90 -0
  58. data/test/releasy/dsl_wrapper_test.rb +79 -0
  59. data/test/releasy/integration/source_test.rb +122 -0
  60. data/test/releasy/mixins/register_test.rb +52 -0
  61. data/test/releasy/project_test.rb +198 -0
  62. data/test/releasy/windows_wrapper_maker_test.rb +61 -0
  63. data/test/teststrap.rb +52 -0
  64. data/test/yard_test.rb +61 -0
  65. data/test_project/Gemfile +9 -0
  66. data/test_project/LICENSE.txt +1 -0
  67. data/test_project/README.txt +2 -0
  68. data/test_project/bin/test_app +3 -0
  69. data/test_project/lib/test_app.rb +6 -0
  70. data/test_project/lib/test_app/stuff.rb +1 -0
  71. data/test_project/test_app.icns +0 -0
  72. data/test_project/test_app.ico +0 -0
  73. data/wrappers/put_wrappers_here_for_testing.txt +17 -0
  74. metadata +236 -0
@@ -0,0 +1,150 @@
1
+ require "releasy/builders/windows_builder"
2
+ require "releasy/mixins/has_gemspecs"
3
+
4
+ module Releasy
5
+ module Builders
6
+ # Wraps up an application for Windows when not on Windows, based on a RubyInstaller distribution and installing Windows binary gems.
7
+ #
8
+ # The resulting package will be much larger than a Windows package created on Windows, since it will include
9
+ # the whole Ruby distribution, not just files that are needed.
10
+ #
11
+ # Limitations:
12
+ # * Does not DLLs loaded from the system, which will have to be included manually if any are required by the application and no universally available in Windows installations.
13
+ # * Unless a gem is in pure Ruby or available as a pre-compiled binary gem, it won't work!
14
+ class WindowsFolderFromRubyDist < WindowsBuilder
15
+ include Mixins::HasGemspecs
16
+
17
+ TYPE = :windows_folder_from_ruby_dist
18
+ DEFAULT_FOLDER_SUFFIX = "WIN32"
19
+
20
+ # Files that are required for Tcl/Tk, but which are unlikely to be used in many applications.
21
+ TCL_TK_FILES = %w[bin/tcl*-ri.dll bin/tk*-ri.dll
22
+ lib/tcltk
23
+ lib/ruby/tk*.rb
24
+ lib/ruby/1.*/tk* lib/ruby/1.*/tcl*
25
+ lib/ruby/1.*/i386-mingw32/tk* lib/ruby/1.*/i386-mingw32/tcl*
26
+ ]
27
+
28
+ # Encoding files that are required, even if we don't need most of them if we select to {#exclude_encoding}.
29
+ REQUIRED_ENCODING_FILES = %w[encdb.so iso_8859_1.so utf_16le.so trans/single_byte.so trans/transdb.so trans/utf_16_32.so]
30
+
31
+ Builders.register self
32
+
33
+ # @return [String] Path to windows distribution archive that has been manually downloaded from http://rubyinstaller.org/downloads/ (e.g. "rubies/ruby-1.9.2-p290-i386-mingw32.7z").
34
+ attr_accessor :ruby_dist
35
+
36
+ # Remove TCL/TK from package, which can save a significant amount of space if the application does not require them.
37
+ # This is over 6MB uncompressed, which is a saving of 1.6MB when compressed with 7z format (LZMA).
38
+ # @return [Project] self
39
+ def exclude_tcl_tk; @exclude_tcl_tk = true; self; end
40
+
41
+ def valid_for_platform?; not Releasy.win_platform?; end
42
+
43
+ protected
44
+ # FOLDER containing EXE, Ruby + source.
45
+ def generate_tasks
46
+ raise ConfigError, "#ruby_dist not set" unless ruby_dist
47
+ raise ConfigError, "#ruby_dist not valid: #{ruby_dist}" unless File.exist?(ruby_dist) and File.extname(ruby_dist) == ".7z"
48
+
49
+ directory project.output_path
50
+
51
+ file folder => project.files + [ruby_dist] do
52
+ build
53
+ end
54
+
55
+ desc "Build source/exe folder #{project.version} from wrapper"
56
+ task "build:windows:folder_from_ruby_dist" => folder
57
+ end
58
+
59
+ protected
60
+ def build
61
+ Rake::FileUtilsExt.verbose project.verbose?
62
+
63
+ copy_ruby_distribution
64
+ delete_excluded_files
65
+
66
+ copy_files_relative project.files, File.join(folder, 'src')
67
+
68
+ create_link_files folder
69
+ project.exposed_files.each {|file| cp file, folder }
70
+
71
+ create_executable
72
+
73
+ # Copy gems.
74
+ destination = File.join(folder, 'vendor')
75
+ downloaded_binary_gems = install_binary_gems destination
76
+ copy_gems vendored_gem_names(downloaded_binary_gems), destination
77
+ end
78
+
79
+ protected
80
+ def delete_excluded_files
81
+ # Remove TCL/TK dlls, lib folder and source.
82
+ rm_r Dir[*(TCL_TK_FILES.map {|f| File.join(folder, f) })].uniq.sort if @exclude_tcl_tk
83
+
84
+ # Remove Encoding files on Ruby 1.9
85
+ if encoding_excluded? and ruby_dist =~ /1\.9\.\d/
86
+ encoding_files = Dir[File.join folder, "lib/ruby/1.9.1/i386-mingw32/enc/**/*.so"]
87
+ required_encoding_files = REQUIRED_ENCODING_FILES.map {|f| File.join folder, "lib/ruby/1.9.1/i386-mingw32/enc", f }
88
+ rm_r encoding_files - required_encoding_files
89
+ end
90
+ end
91
+
92
+ protected
93
+ def setup
94
+ @exclude_tcl_tk = false
95
+ @ruby_dist = nil
96
+ super
97
+ end
98
+
99
+ protected
100
+ def executable_name; "#{project.underscored_name}.exe"; end
101
+
102
+ protected
103
+ def copy_ruby_distribution
104
+ archive_name = File.basename(ruby_dist).chomp(File.extname(ruby_dist))
105
+ exec %[7z x "#{ruby_dist}" -o"#{File.dirname folder}"]
106
+ mv File.join(File.dirname(folder), archive_name), folder
107
+ rm_r File.join(folder, "share")
108
+ rm_r File.join(folder, "include") if File.exists? File.join(folder, "include")
109
+ unused_exe = effective_executable_type == :windows ? "ruby.exe" : "rubyw.exe"
110
+ rm File.join(folder, "bin", unused_exe)
111
+ end
112
+
113
+ protected
114
+ def create_executable
115
+ maker = Releasy::WindowsWrapperMaker.new
116
+ maker.build_executable("#{folder}/#{executable_name}", "src/#{project.executable}",
117
+ :windows => (effective_executable_type == :windows))
118
+ end
119
+
120
+ protected
121
+ def install_binary_gems(destination)
122
+ puts "Checking gems to see if any are binary" if project.verbose?
123
+ binary_gems = []
124
+ gemspecs.reject {|s| false }.each do |spec|
125
+ puts "Checking gem #{spec.name} #{spec.version} to see if there is a Windows binary version" if project.verbose?
126
+ # Find out what versions are available and if the required version is available as a windows binary, download and install that.
127
+ versions = %x[gem list "#{spec.name}" --remote --all --prerelease]
128
+ if versions =~ /#{spec.name} \(([^\)]*)\)/m
129
+ version_string = $1
130
+ platforms = version_string.split(/,\s*/).find {|s| s =~ /^#{spec.version}/ }.split(/\s+/)
131
+ windows_platform = platforms.find {|p| p =~ /mingw|mswin/ }
132
+ raise "Gem #{spec.name} is binary, but #{spec.version} does not have a published binary" if version_string =~ /mingw|mswin/ and not windows_platform
133
+
134
+ if windows_platform
135
+ puts "Installing Windows version of binary gem #{spec.name} #{spec.version}"
136
+ # If we have a bundle file specified, then gem will _only_ install the version specified by it and not the one we request.
137
+ bundle_gemfile = ENV['BUNDLE_GEMFILE']
138
+ ENV['BUNDLE_GEMFILE'] = ''
139
+ exec %[gem install "#{spec.name}" --remote --no-rdoc --no-ri --force --ignore-dependencies --platform "#{windows_platform}" --version "#{spec.version}" --install-dir "#{destination}"]
140
+ ENV['BUNDLE_GEMFILE'] = bundle_gemfile
141
+ binary_gems << spec.name
142
+ end
143
+ end
144
+ end
145
+
146
+ binary_gems
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,117 @@
1
+ require "releasy/builders/ocra_builder"
2
+
3
+ module Releasy
4
+ module Builders
5
+ # Builds a win32 installer for the application.
6
+ class WindowsInstaller < OcraBuilder
7
+ TYPE = :windows_installer
8
+ INSTALLER_SCRIPT = "windows_installer.iss"
9
+ DEFAULT_FOLDER_SUFFIX = "WIN32_INSTALLER"
10
+
11
+ Builders.register self
12
+
13
+ # @return [String] Optional start-menu grouping of the application when installed (if name == "app" and installer_group == "frog", then it will get put into 'frog/app' in the start menu).
14
+ attr_accessor :start_menu_group
15
+
16
+ # @return [String] File name of readme file - End user will have the option to view this after the Windows installer has installed, recommended to be .txt, .rtf or .html.
17
+ attr_accessor :readme
18
+ # @return [String] Filename of license file - Must be .txt or .rtf file, which will be shown to user who will be requested to accept it (Windows installer only).
19
+ attr_accessor :license
20
+
21
+ protected
22
+ # Regular windows installer, but some users consider them evil.
23
+ def generate_tasks
24
+ directory folder
25
+
26
+ file folder => project.files do
27
+ Rake::FileUtilsExt.verbose project.verbose?
28
+
29
+ create_link_files folder
30
+ project.exposed_files.each {|file| cp file, folder }
31
+
32
+ create_installer installer_name, :links => true
33
+
34
+ rm temp_installer_script
35
+ end
36
+
37
+ desc "Build installer #{project.version} [Innosetup]"
38
+ task "build:windows:installer" => folder
39
+ end
40
+
41
+ protected
42
+ def setup
43
+ super
44
+ @start_menu_group = nil
45
+ @readme = nil
46
+ @license = nil
47
+ end
48
+
49
+ protected
50
+ def temp_installer_script; "#{project.output_path}/#{INSTALLER_SCRIPT}"; end
51
+ def installer_name; "#{folder}/#{project.underscored_name}_setup.exe"; end
52
+
53
+ protected
54
+ def create_installer(file, options = {})
55
+ generate_installer_script file, options
56
+ exec %[#{ocra_command} --chdir-first --no-lzma --innosetup "#{temp_installer_script}"]
57
+ end
58
+
59
+ # Generate innosetup script to build installer.
60
+ protected
61
+ def generate_installer_script(output_file, options = {})
62
+ installer_links = options[:links]
63
+
64
+ File.open(temp_installer_script, "w") do |file|
65
+ file.write <<END
66
+ [Setup]
67
+ AppName=#{project.name}
68
+ AppVersion=#{project.version}
69
+ DefaultDirName={pf}\\#{project.name.gsub(/[^\w\s]/, '')}
70
+ OutputDir=#{File.dirname output_file}
71
+ OutputBaseFilename=#{File.basename(output_file).chomp(File.extname(output_file))}
72
+ UninstallDisplayIcon={app}\\#{project.underscored_name}.exe
73
+ END
74
+
75
+ if installer_links
76
+ if start_menu_group
77
+ file.puts "DefaultGroupName=#{start_menu_group}\\#{project.name}"
78
+ else
79
+ file.puts "DefaultGroupName=#{project.name}"
80
+ end
81
+
82
+ file.puts "LicenseFile=#{license}" if license # User must accept license.
83
+ file.puts "SetupIconFile=#{icon}" if icon
84
+ file.puts
85
+
86
+ file.puts "[Files]"
87
+
88
+ file.puts %[Source: "#{license}"; DestDir: "{app}"] if license
89
+ file.puts %[Source: "#{readme}"; DestDir: "{app}"; Flags: isreadme] if readme
90
+
91
+ dir = File.dirname(output_file).tr("/", "\\")
92
+ project.send(:links).each_value do |title|
93
+ file.puts %[Source: "#{dir}\\#{title}.url"; DestDir: "{app}"]
94
+ end
95
+ end
96
+
97
+ file.write <<END
98
+
99
+ [Run]
100
+ Filename: "{app}\\#{project.underscored_name}.exe"; Description: "Launch"; Flags: postinstall nowait skipifsilent unchecked
101
+
102
+ [Icons]
103
+ Name: "{group}\\#{project.name}"; Filename: "{app}\\#{project.underscored_name}.exe"
104
+ Name: "{group}\\Uninstall #{project.name}"; Filename: "{uninstallexe}"
105
+ Name: "{commondesktop}\\#{project.name}"; Filename: "{app}\\#{project.underscored_name}.exe"; Tasks: desktopicon
106
+
107
+ [Tasks]
108
+ Name: desktopicon; Description: "Create a &desktop icon"; GroupDescription: "Additional icons:";
109
+ Name: desktopicon\\common; Description: "For all users"; GroupDescription: "Additional icons:"; Flags: exclusive
110
+ Name: desktopicon\\user; Description: "For the current user only"; GroupDescription: "Additional icons:"; Flags: exclusive unchecked
111
+ Name: quicklaunchicon; Description: "Create a &Quick Launch icon"; GroupDescription: "Additional icons:"; Flags: unchecked
112
+ END
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,37 @@
1
+ require "releasy/builders/ocra_builder"
2
+
3
+ module Releasy
4
+ module Builders
5
+ # Creates a completely standalone Windows executable.
6
+ #
7
+ # @note Startup of the executable created by this build takes a couple of seconds longer than running from the other windows builds, as the files are extracted into a temporary directory each time it is run. It is recommended to build with _:windows_folder_ or _:windows_installer_ instead of this, unless you really need to distribute the application as a single file.
8
+ class WindowsStandalone < OcraBuilder
9
+ TYPE = :windows_standalone
10
+ DEFAULT_FOLDER_SUFFIX = "WIN32_EXE"
11
+
12
+ Builders.register self
13
+
14
+ protected
15
+ # Self-extracting standalone executable.
16
+ def generate_tasks
17
+ directory folder
18
+
19
+ file folder => project.files do
20
+ Rake::FileUtilsExt.verbose project.verbose?
21
+
22
+ project.exposed_files.each {|file| cp file, folder }
23
+
24
+ create_link_files folder
25
+
26
+ exec %[#{ocra_command} --output "#{folder}/#{executable_name}"]
27
+ end
28
+
29
+ desc "Build standalone exe #{project.version} [Ocra]"
30
+ task "build:windows:standalone" => folder
31
+ end
32
+
33
+ protected
34
+ def executable_name; "#{project.underscored_name}.exe"; end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ require 'cri'
2
+
3
+ Releasy::Cli = Cri::Command.new_basic_root.modify do
4
+ name 'releasy'
5
+ usage 'releasy [options] [command] [options]'
6
+ summary 'helper for using the Releasy gem'
7
+ description 'Helper for using the Releasy gem to release packages'
8
+ end
9
+
10
+ Dir[File.expand_path("../cli/*.rb", __FILE__)].each do |file|
11
+ require file.chomp(File.extname(file))
12
+ end
@@ -0,0 +1,66 @@
1
+ require 'cri'
2
+ require "releasy/archivers/exe"
3
+
4
+ command = 'install-sfx'
5
+ sfx_file = Releasy::Archivers::Exe::SFX_NAME
6
+ sfx_path = Releasy::Archivers::Exe::SFX_FILE
7
+
8
+ Releasy::Cli.define_command do
9
+ name command
10
+ usage "#{command} [options]"
11
+ aliases
12
+ summary "copy #{sfx_file} to 7z assets folder"
13
+ description "copy #{sfx_file} to 7z assets folder, after 7z has been installed (required only when not on a Windows system). Warning: This command will likely require 'sudo' to be able to write the file"
14
+
15
+ flag :h, :help, 'show help for this command' do |value, cmd|
16
+ puts cmd.help
17
+ exit 0
18
+ end
19
+
20
+ option :t, :output, "specify directory to copy to (default is to try to find it automatically)", :argument => :required
21
+
22
+ run do |options, args, cmd|
23
+ if Gem.win_platform?
24
+ puts "#{command}: only required when not on a Windows platform, since #{sfx_file} is included in the Windows version of 7z"
25
+ exit 0
26
+ end
27
+
28
+ exe_location = `which 7z`.strip
29
+
30
+ if exe_location.empty?
31
+ puts "#{command}: 7z (p7zip) not installed; install it before trying to use this command"
32
+ exit 0
33
+ end
34
+
35
+ assets_location = if options[:output]
36
+ options[:output]
37
+ elsif exe_location =~ %r[bin/7z$]
38
+ exe_location.sub %r[bin/7z$], "lib/p7zip"
39
+ else
40
+ nil
41
+ end
42
+
43
+ destination_file = File.join(assets_location, sfx_file)
44
+ if File.exists? destination_file
45
+ puts "#{command}: #{destination_file} already exists; no need to install it again"
46
+ exit 0
47
+ else
48
+ begin
49
+ FileUtils.cp sfx_path, assets_location, :verbose => true
50
+ puts "#{sfx_file} copied to #{assets_location}"
51
+ rescue Errno::ENOENT, Errno::EACCES
52
+
53
+ if ENV["USER"] != "root"
54
+ command = %[sudo cp "#{sfx_path}" "#{assets_location}"]
55
+ puts "Copy failed, trying again as super-user:\n#{command}"
56
+ exec command
57
+ if File.exists? destination_file
58
+ puts "#{sfx_file} copied to #{assets_location}"
59
+ else
60
+ puts "Failed copy as super-user :("
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,71 @@
1
+ module Releasy
2
+ # Wraps an object and redirects public methods to it, to allow for a terse, block-based API.
3
+ #
4
+ # * Safer alternative to running Object#instance_eval directly, since protected/private methods and instance variables are not exposed.
5
+ # * Less wordy than a system which operates like Object#tap (`object.tap {|o| o.fish = 5; o.run }`)
6
+ #
7
+ # A method call, #meth called on the wrapper will try to call #meth or #meth= on the owner, as appropriate.
8
+ #
9
+ # @example
10
+ # # To create a DSL block for a given object.
11
+ # class Cheese
12
+ # attr_accessor :value
13
+ # attr_accessor :list
14
+ # def initialize; @value = 0; @list = []; end
15
+ # def invert; @list.reverse!; end
16
+ # end
17
+ #
18
+ # object = Cheese.new
19
+ # Releasy::DSLWrapper.wrap object do
20
+ # list [1, 2, 3] # Calls object.list = [1, 2, 3]
21
+ # list << 4 # Calls object.list << 4
22
+ # value 5 # Calls object.value = 5
23
+ # value list.size # Calls object.value = object.list.size
24
+ # invert # Calls object.invert
25
+ # end
26
+ #
27
+ class DSLWrapper
28
+ # @return [Object] Object that the DSLWrapper object is redirecting to.
29
+ attr_reader :owner
30
+
31
+ class << self
32
+ # Synonym for .new.
33
+ alias_method :wrap, :new
34
+ end
35
+
36
+ # If passed a block, the DSLWrapper will #instance_eval it automatically.
37
+ #
38
+ # @param owner [Object] Object to redirect the public methods of.
39
+ def initialize(owner, &block)
40
+ @owner = owner
41
+
42
+ metaclass = class << self; self; end
43
+
44
+ (@owner.public_methods - Object.public_instance_methods).each do |target_method|
45
+ redirection_method = target_method.to_s.chomp('=').to_sym
46
+
47
+ metaclass.class_eval do
48
+ define_method redirection_method do |*args, &inner_block|
49
+ if @owner.respond_to? "#{redirection_method}=" and (args.any? or not @owner.respond_to? redirection_method)
50
+ # Has a setter and we are passing argument(s) or if we haven't got a corresponding getter.
51
+ @owner.send "#{redirection_method}=", *args, &inner_block
52
+ elsif @owner.respond_to? redirection_method
53
+ # We have a getter or general method
54
+ @owner.send redirection_method, *args, &inner_block
55
+ else
56
+ # Should never reach here, but let's be paranoid.
57
+ raise NoMethodError, "#{owner} does not have a public method, ##{redirection_method}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ instance_eval &block if block_given?
64
+ end
65
+
66
+ private
67
+ def method_missing(meth, *args, &block)
68
+ raise NoMethodError, "#{owner} does not have either public method, ##{meth} or ##{meth}="
69
+ end
70
+ end
71
+ end