releasy 0.2.0rc1

Sign up to get free protection for your applications and to get access to all the features.
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