diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml index 03b399ec..f32ba0f2 100644 --- a/.github/workflows/alpine.yml +++ b/.github/workflows/alpine.yml @@ -60,7 +60,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VER: 17 + CACHE_VER: 19 TZ: "Etc/UTC" VERBOSE: no diff --git a/.github/workflows/gem-test-and-release.yml b/.github/workflows/gem-test-and-release.yml index 6af995f5..e4f6cf3c 100644 --- a/.github/workflows/gem-test-and-release.yml +++ b/.github/workflows/gem-test-and-release.yml @@ -49,7 +49,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VER: 16 + CACHE_VER: 19 DEBIAN_FRONTEND: "noninteractive" TZ: "Etc/UTC" # show cmake output diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d0e9a670..ae361d4b 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -58,7 +58,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VER: 17 + CACHE_VER: 19 VERBOSE: no jobs: diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 420afb19..e8d2f751 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -60,7 +60,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VER: 17 + CACHE_VER: 19 DEBIAN_FRONTEND: "noninteractive" TZ: "Etc/UTC" # show cmake output (yes/no) diff --git a/.github/workflows/windows-msys.yml b/.github/workflows/windows-msys.yml index 80f13ba2..9a9ab71c 100644 --- a/.github/workflows/windows-msys.yml +++ b/.github/workflows/windows-msys.yml @@ -60,7 +60,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VER: 17 + CACHE_VER: 19 VERBOSE: no jobs: diff --git a/CMakeLists.txt b/CMakeLists.txt index d80ae4e7..0b6d4be7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -431,7 +431,7 @@ if (${SETUP_MODE}) add_dependencies(tebako-fs packaged_filesystem) - add_custom_target(tebako COMMAND ruby ${EXE}/tebako-packager finalize ${OSTYPE_TXT} ${RUBY_SOURCE_DIR} ${APP_NAME} ${RUBY_VER} ${DEPS_BIN_DIR}/patchelf ${WITH_PATCHELF}) + add_custom_target(tebako COMMAND ruby ${EXE}/tebako-packager finalize ${RUBY_SOURCE_DIR} ${APP_NAME} ${RUBY_VER} ${DEPS_BIN_DIR}/patchelf ${WITH_PATCHELF}) add_dependencies(tebako setup tebako-fs) endif(${SETUP_MODE}) diff --git a/exe/tebako-packager b/exe/tebako-packager index af0d228c..a1ec6cb8 100755 --- a/exe/tebako-packager +++ b/exe/tebako-packager @@ -70,19 +70,18 @@ begin when "finalize" # ARGV[0] -- command - # ARGV[1] -- OSTYPE - # ARGV[2] -- RUBY_SOURCE_DIR - # ARGV[3] -- APP_NAME - # ARGV[4] -- RUBY_VER - # ARGV[5] -- patchelf executable - # ARGV[6] -- WITH_PATHELF - unless ARGV.length == 7 + # ARGV[1] -- RUBY_SOURCE_DIR + # ARGV[2] -- APP_NAME + # ARGV[3] -- RUBY_VER + # ARGV[4] -- patchelf executable + # ARGV[5] -- WITH_PATHELF + unless ARGV.length == 6 raise Tebako::Error, - "tebako-packager finalize command expects 7 arguments, #{ARGV.length} has been provided." + "tebako-packager finalize command expects 6 arguments, #{ARGV.length} has been provided." end - ruby_ver = Tebako::RubyVersion.new(ARGV[4]) - with_patchelf = ARGV[6].casecmp("ON").zero? || ARGV[6].casecmp("YES").zero? - Tebako::Packager.finalize(ARGV[1], ARGV[2], ARGV[3], ruby_ver, with_patchelf ? ARGV[5] : nil) + ruby_ver = Tebako::RubyVersion.new(ARGV[3]) + with_patchelf = ARGV[5].casecmp("ON").zero? || ARGV[5].casecmp("YES").zero? + Tebako::Packager.finalize(ARGV[1], ARGV[2], ruby_ver, with_patchelf ? ARGV[4] : nil) else raise Tebako::Error, "tebako-packager cannot process #{ARGV[0]} command" end diff --git a/lib/tebako/build_helpers.rb b/lib/tebako/build_helpers.rb index 835e4dc1..80c8801e 100755 --- a/lib/tebako/build_helpers.rb +++ b/lib/tebako/build_helpers.rb @@ -32,20 +32,6 @@ module Tebako # Ruby build helpers module BuildHelpers class << self - def ncores - if RUBY_PLATFORM.include?("darwin") - out, st = Open3.capture2e("sysctl", "-n", "hw.ncpu") - else - out, st = Open3.capture2e("nproc", "--all") - end - - if !st.signaled? && st.exitstatus.zero? - out.strip.to_i - else - 4 - end - end - def run_with_capture(args) args = args.compact puts " ... @ #{args.join(" ")}" diff --git a/lib/tebako/cli.rb b/lib/tebako/cli.rb index ff1dcaa0..5f776208 100755 --- a/lib/tebako/cli.rb +++ b/lib/tebako/cli.rb @@ -28,6 +28,8 @@ require "digest" require "fileutils" +require "find" +require "pathname" require "open3" require "thor" require "yaml" @@ -36,6 +38,7 @@ require_relative "cli_helpers" require_relative "error" require_relative "ruby_version" +require_relative "scenario_manager" require_relative "version" # Tebako - an executable packager @@ -66,10 +69,11 @@ def clean_ruby (om,) = bootstrap(clean: true) suffix = options["Ruby"].nil? ? "" : "_#{options["Ruby"]}" - nmr = "src/_ruby#{suffix}*" - nms = "stash#{suffix}*" - FileUtils.rm_rf(Dir.glob(File.join(om.deps, nmr)), secure: true) - FileUtils.rm_rf(Dir.glob(File.join(om.deps, nms)), secure: true) + nmr = Dir.glob(File.join(om.deps, "src", "_ruby#{suffix}*")) + nms = Dir.glob(File.join(om.deps, "stash#{suffix}*")) + + FileUtils.rm_rf(nmr + nms, secure: true) + extra_win_clean(nmr) end desc "hash", "Print build script hash (ci cache key)" @@ -147,13 +151,29 @@ def bootstrap(clean: false) [options_manager, cache_manager] end + # Ruby extension maker sometimes creates files with 'NUL' name on Windows + # This method removes such files + def extra_win_clean(nmr) + return unless nmr.any? && ScenarioManagerBase.new.msys? + + nmr.each do |path| + if File.basename(path) == "NUL" + full_path = "//?/#{path}" + FileUtils.rm_f(full_path) + end + end + FileUtils.rm_rf(nmr, secure: true) + end + def initialize(*args) super return if args[2][:current_command].name.include?("hash") puts "Tebako executable packager version #{Tebako::VERSION}" end + end + no_commands do def options original_options = super tebafile = original_options["tebafile"].nil? ? DEFAULT_TEBAFILE : original_options["tebafile"] @@ -164,9 +184,7 @@ def options original_options end end - end - no_commands do def source c_path = Pathname.new(__FILE__).realpath @source ||= File.expand_path("../../..", c_path) @@ -177,7 +195,10 @@ def validate_press_options opts = "" opts += " '--root'" if options["root"].nil? - opts += " '--entry-point'" if options["entry-point"].nil? + if options["entry-point"].nil? + opts += ", " unless opts.empty? + opts += " '--entry-point'" + end raise Thor::Error, "No value provided for required options #{opts}" unless opts.empty? end end diff --git a/lib/tebako/cli_helpers.rb b/lib/tebako/cli_helpers.rb index d3ae05f5..ec40e5dd 100644 --- a/lib/tebako/cli_helpers.rb +++ b/lib/tebako/cli_helpers.rb @@ -82,7 +82,7 @@ def do_press_runtime(options_manager, scenario_manager) return unless %w[both runtime bundle].include?(options_manager.mode) generate_files(options_manager, scenario_manager) - merged_env = ENV.to_h.merge(options_manager.b_env) + merged_env = ENV.to_h.merge(scenario_manager.b_env) Tebako.packaging_error(103) unless system(merged_env, press_cfg_cmd(options_manager)) Tebako.packaging_error(104) unless system(merged_env, press_build_cmd(options_manager)) end @@ -90,7 +90,7 @@ def do_press_runtime(options_manager, scenario_manager) def do_setup(options_manager) puts "Setting up tebako packaging environment" - merged_env = ENV.to_h.merge(options_manager.b_env) + merged_env = ENV.to_h.merge(Tebako::ScenarioManagerBase.new.b_env) Tebako.packaging_error(101) unless system(merged_env, setup_cfg_cmd(options_manager)) Tebako.packaging_error(102) unless system(merged_env, setup_build_cmd(options_manager)) end diff --git a/lib/tebako/deploy_helper.rb b/lib/tebako/deploy_helper.rb index 0ea4769e..38e0736c 100644 --- a/lib/tebako/deploy_helper.rb +++ b/lib/tebako/deploy_helper.rb @@ -48,7 +48,6 @@ def initialize(fs_root, fs_entrance, target_dir, pre_dir) @target_dir = target_dir @pre_dir = pre_dir @verbose = %w[yes true].include?(ENV.fetch("VERBOSE", nil)) - @ncores = BuildHelpers.ncores end attr_reader :gem_home @@ -162,7 +161,7 @@ def collect_and_deploy_gem_and_gemfile(gemspec) Dir.chdir(@pre_dir) do bundle_config puts " *** It may take a long time for a big project. It takes REALLY long time on Windows ***" - BuildHelpers.run_with_capture_v([@bundler_command, bundler_reference, "install", "--jobs=#{@ncores}"]) + BuildHelpers.run_with_capture_v([@bundler_command, bundler_reference, "install", "--jobs=#{ncores}"]) BuildHelpers.run_with_capture_v([@bundler_command, bundler_reference, "exec", @gem_command, "build", gemspec]) install_all_gems_or_fail end @@ -222,7 +221,7 @@ def deploy_gemfile Dir.chdir(@tld) do bundle_config puts " *** It may take a long time for a big project. It takes REALLY long time on Windows ***" - BuildHelpers.run_with_capture_v([@bundler_command, bundler_reference, "install", "--jobs=#{@ncores}"]) + BuildHelpers.run_with_capture_v([@bundler_command, bundler_reference, "install", "--jobs=#{ncores}"]) end check_entry_point("local") diff --git a/lib/tebako/options_manager.rb b/lib/tebako/options_manager.rb index 3b587ba2..092aad64 100644 --- a/lib/tebako/options_manager.rb +++ b/lib/tebako/options_manager.rb @@ -44,19 +44,11 @@ def initialize(options) @options = options @rv = Tebako::RubyVersion.new(@options["Ruby"]) @ruby_ver, @ruby_hash = @rv.extend_ruby_version + @scmb = ScenarioManagerBase.new end attr_reader :ruby_ver, :rv - def b_env - u_flags = if RbConfig::CONFIG["host_os"] =~ /darwin/ - "-DTARGET_OS_SIMULATOR=0 -DTARGET_OS_IPHONE=0 #{ENV.fetch("CXXFLAGS", nil)}" - else - ENV.fetch("CXXFLAGS", nil) - end - @b_env ||= { "CXXFLAGS" => u_flags } - end - def cfg_options ## {v_parts[3]} may be something like rc1 that won't work with CMake v_parts = Tebako::VERSION.split(".") @@ -64,7 +56,7 @@ def cfg_options # So we have to use \"xxx\" @cfg_options ||= "-DCMAKE_BUILD_TYPE=Release -DRUBY_VER:STRING=\"#{@ruby_ver}\" -DRUBY_HASH:STRING=\"#{@ruby_hash}\" " \ - "-DDEPS:STRING=\"#{deps}\" -G \"#{m_files}\" -B \"#{output_folder}\" -S \"#{source}\" " \ + "-DDEPS:STRING=\"#{deps}\" -G \"#{@scmb.m_files}\" -B \"#{output_folder}\" -S \"#{source}\" " \ "#{remove_glibc_private} -DTEBAKO_VERSION:STRING=\"#{v_parts[0]}.#{v_parts[1]}.#{v_parts[2]}\"" end @@ -127,7 +119,7 @@ def deps_lib_dir def fs_current fs_current = Dir.pwd - if RUBY_PLATFORM =~ /msys|mingw|cygwin/ + if @scmb.msys? fs_current, cygpath_res = Open3.capture2e("cygpath", "-w", fs_current) Tebako.packaging_error(101) unless cygpath_res.success? fs_current.strip! @@ -158,20 +150,6 @@ def mode @mode ||= @options["mode"].nil? ? "bundle" : @options["mode"] end - def m_files - # [TODO] - # Ninja generates incorrect script for tebako press target -- gets lost in a chain custom targets - # Using makefiles has negative performance impact so it needs to be fixed - @m_files ||= case RUBY_PLATFORM - when /linux/, /darwin/ - "Unix Makefiles" - when /msys|mingw|cygwin/ - "MinGW Makefiles" - else - raise Tebako::Error.new("#{RUBY_PLATFORM} is not supported.", 112) - end - end - def output_folder @output_folder ||= File.join(prefix, "o") end diff --git a/lib/tebako/packager.rb b/lib/tebako/packager.rb index 12401fd9..b8a1ce99 100644 --- a/lib/tebako/packager.rb +++ b/lib/tebako/packager.rb @@ -35,7 +35,7 @@ require_relative "stripper" require_relative "packager/pass1_patch" require_relative "packager/pass1a_patch" -require_relative "packager/pass2" +require_relative "packager/pass2_patch" require_relative "packager/patch_helpers" # Tebako - an executable packager @@ -93,15 +93,16 @@ def do_patch(patch_map, root) patch_map.each { |fname, mapping| PatchHelpers.patch_file("#{root}/#{fname}", mapping) } end - def finalize(os_type, src_dir, app_name, ruby_ver, patchelf) + def finalize(src_dir, app_name, ruby_ver, patchelf) puts "-- Running finalize script" RubyBuilder.new(ruby_ver, src_dir).target_build - exe_suffix = Packager::PatchHelpers.exe_suffix(os_type) + exe_suffix = ScenarioManagerBase.new.exe_suffix src_name = File.join(src_dir, "ruby#{exe_suffix}") patchelf(src_name, patchelf) package_name = "#{app_name}#{exe_suffix}" - strip_or_copy(os_type, src_name, package_name) + # strip_or_copy(os_type, src_name, package_name) + Tebako::Stripper.strip_file(src_name, package_name) puts "Created tebako package at \"#{package_name}\"" end @@ -149,7 +150,8 @@ def pass1a(ruby_source_dir) def pass2(ostype, ruby_source_dir, deps_lib_dir, ruby_ver) puts "-- Running pass2 script" - do_patch(Pass2.get_patch_map(ostype, deps_lib_dir, ruby_ver), ruby_source_dir) + patch = Pass2Patch.new(ostype, deps_lib_dir, ruby_ver).patch_map + do_patch(patch, ruby_source_dir) end # Stash @@ -194,15 +196,15 @@ def patchelf(src_name, patchelf) BuildHelpers.run_with_capture(params) end - def strip_or_copy(os_type, src_name, package_name) - # [TODO] On MSys strip sometimes creates a broken executable - # https://github.com/tamatebako/tebako/issues/172 - if Packager::PatchHelpers.msys?(os_type) - FileUtils.cp(src_name, package_name) - else - Tebako::Stripper.strip_file(src_name, package_name) - end - end + # def strip_or_copy(_os_type, src_name, package_name) + # [TODO] On MSys strip sometimes creates a broken executable + # https://github.com/tamatebako/tebako/issues/172 + # if Packager::PatchHelpers.msys?(os_type) + # FileUtils.cp(src_name, package_name) + # else + # Tebako::Stripper.strip_file(src_name, package_name) + # end + # end end end end diff --git a/lib/tebako/packager/pass1_patch.rb b/lib/tebako/packager/pass1_patch.rb index 010ae284..1f75cf00 100644 --- a/lib/tebako/packager/pass1_patch.rb +++ b/lib/tebako/packager/pass1_patch.rb @@ -202,23 +202,21 @@ def patch_map def gnumakefile_in_patch_p1 # rubocop:disable Metrics/MethodLength objext = @ruby_ver.ruby32? ? "$(OBJEXT)" : "@OBJEXT@" { - " DLLWRAP += -mno-cygwin" => - "# tebako patched DLLWRAP += -mno-cygwin", + "$(Q) $(DLLWRAP) \\" => GNUMAKEFILE_IN_DLLTOOL_SUBST, "$(WPROGRAM): $(RUBYW_INSTALL_NAME).res.#{objext}" => "$(WPROGRAM): $(RUBYW_INSTALL_NAME).res.#{objext} $(WINMAINOBJ) # tebako patched", "$(MAINOBJ) $(EXTOBJS) $(LIBRUBYARG) $(LIBS) -o $@" => + "$(WINMAINOBJ) $(EXTOBJS) $(LIBRUBYARG) $(LIBS) -o $@ # tebako patched", - "--output-exp=$(RUBY_EXP) \\" => - "--output-exp=$(RUBY_EXP) --output-lib=$(LIBRUBY) --output-def=tebako.def \\", + "--output-exp=$(RUBY_EXP) \\" => "# tebako patched --output-exp=$(RUBY_EXP) \\", "--export-all $(LIBRUBY_A) $(LIBS) -o $(PROGRAM)" => - "--export-all $(LIBRUBY_A) $(LIBS) -o program-stub.exe # tebako patched", + "# tebako patched --export-all $(LIBRUBY_A) $(LIBS) -o $(PROGRAM)", - "@rm -f $(PROGRAM)" => - "@rm -f program-stub.exe # tebako patched", + "@rm -f $(PROGRAM)" => "# tebako patched @rm -f $(PROGRAM)", " $(Q) $(LDSHARED) $(DLDFLAGS) $(OBJS) dmyext.o $(SOLIBS) -o $(PROGRAM)" => "# tebako patched $(Q) $(LDSHARED) $(DLDFLAGS) $(OBJS) dmyext.o $(SOLIBS) -o $(PROGRAM)", diff --git a/lib/tebako/packager/pass2.rb b/lib/tebako/packager/pass2.rb deleted file mode 100755 index 8f56b03e..00000000 --- a/lib/tebako/packager/pass2.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2021-2024 [Ribose Inc](https://www.ribose.com). -# All rights reserved. -# This file is a part of tebako -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -require_relative "patch_literals" -require_relative "patch_main" -require_relative "patch_libraries" -require_relative "patch_helpers" -require_relative "patch_buildsystem" - -# Tebako - an executable packager -module Tebako - module Packager - # Ruby patching definitions (pass2) - module Pass2 - class << self - def get_patch_map(ostype, deps_lib_dir, ruby_ver) - patch_map = get_patch_map_base(ostype, deps_lib_dir, ruby_ver) - patch_map.store("thread_pthread.c", LINUX_MUSL_THREAD_PTHREAD_PATCH) if ostype =~ /linux-musl/ - if PatchHelpers.msys?(ostype) - patch_map.merge!(get_msys_patches(ruby_ver)) - elsif ruby_ver.ruby3x? - patch_map.store("common.mk", COMMON_MK_PATCH) - end - extend_patch_map_r33(patch_map, ostype, deps_lib_dir, ruby_ver) - patch_map.store("prism_compile.c", PRISM_PATCHES) if ruby_ver.ruby34? - patch_map - end - - private - - include Tebako::Packager::PatchBuildsystem - include Tebako::Packager::PatchLiterals - def extend_patch_map_r33(patch_map, ostype, deps_lib_dir, ruby_ver) - if ruby_ver.ruby33? || PatchHelpers.msys?(ostype) - patch_map.store("config.status", - get_config_status_patch(ostype, deps_lib_dir, ruby_ver)) - end - patch_map - end - - def get_dir_c_patch(ostype) - pattern = PatchHelpers.msys?(ostype) ? "/* define system APIs */" : "#ifdef HAVE_GETATTRLIST" - dir_c_patch = PatchHelpers.patch_c_file_pre(pattern) - dir_c_patch.merge!(DIR_C_BASE_PATCH) - dir_c_patch - end - - def get_dln_c_patch(ostype, ruby_ver) - pattern = "#ifndef dln_loaderror" - # Not using substitutions of dlxxx functions on Windows - dln_c_patch = { - pattern => "#{PatchHelpers.msys?(ostype) ? C_FILE_SUBST_LESS : C_FILE_SUBST}\n#{pattern}\n" - } - - if PatchHelpers.msys?(ostype) - patch = ruby_ver.ruby32? ? DLN_C_MSYS_PATCH : DLN_C_MSYS_PATCH_PRE32 - dln_c_patch.merge!(patch) - end - - dln_c_patch - end - - def get_io_c_msys_patch(ruby_ver) - io_c_msys_patch = ruby_ver.ruby32? ? IO_C_MSYS_PATCH : IO_C_MSYS_PATCH_PRE_32 - io_c_msys_patch.merge(IO_C_MSYS_BASE_PATCH) - end - - def get_io_c_patch(ostype, ruby_ver) - io_c_patch = PatchHelpers.patch_c_file_pre("/* define system APIs */") - io_c_patch.merge!(get_io_c_msys_patch(ruby_ver)) if PatchHelpers.msys?(ostype) - io_c_patch - end - - def get_util_c_patch(ruby_ver) - if ruby_ver.ruby31? - PatchHelpers.patch_c_file_post("#endif /* !HAVE_GNU_QSORT_R */") - else - PatchHelpers.patch_c_file_pre("#ifndef S_ISDIR") - end - end - - def get_tool_mkconfig_rb_patch(ostype) - subst = PatchHelpers.msys?(ostype) ? TOOL_MKCONFIG_RB_SUBST_MSYS : TOOL_MKCONFIG_RB_SUBST - { - " if fast[name]" => subst - } - end - - def get_msys_patches(ruby_ver) - { - "cygwin/GNUmakefile.in" => get_gnumakefile_in_patch_p2(ruby_ver), - "ruby.c" => RUBY_C_MSYS_PATCHES, - "win32/file.c" => WIN32_FILE_C_MSYS_PATCHES, - "win32/win32.c" => WIN32_WIN32_C_MSYS_PATCHES - } - end - - def get_patch_map_base(ostype, deps_lib_dir, ruby_ver) - { - "template/Makefile.in" => template_makefile_in_patch(ostype, deps_lib_dir, ruby_ver), - "tool/mkconfig.rb" => get_tool_mkconfig_rb_patch(ostype), - "dir.c" => get_dir_c_patch(ostype), "dln.c" => get_dln_c_patch(ostype, ruby_ver), - "io.c" => get_io_c_patch(ostype, ruby_ver), "main.c" => PatchMain.get_main_c_patch(ruby_ver), - "file.c" => PatchHelpers.patch_c_file_pre("/* define system APIs */"), - "util.c" => get_util_c_patch(ruby_ver) - } - end - - def mlibs_subst(ostype, deps_lib_dir, ruby_ver) - yjit_libs = ruby_ver.ruby32only? ? "$(YJIT_LIBS) " : "" - { - "MAINLIBS = #{yjit_libs}@MAINLIBS@" => - "# -- Start of tebako patch -- \n" \ - "MAINLIBS = #{yjit_libs}#{PatchLibraries.mlibs(ostype, deps_lib_dir, ruby_ver, true)}\n" \ - "# -- End of tebako patch -- \n" - } - end - - def template_makefile_in_patch(ostype, deps_lib_dir, ruby_ver) - template_makefile_in_patch_two(ostype, ruby_ver).merge(mlibs_subst(ostype, deps_lib_dir, ruby_ver)) - end - end - end - end -end diff --git a/lib/tebako/packager/pass2_patch.rb b/lib/tebako/packager/pass2_patch.rb new file mode 100644 index 00000000..108d51db --- /dev/null +++ b/lib/tebako/packager/pass2_patch.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +# Copyright (c) 2021-2024 [Ribose Inc](https://www.ribose.com). +# All rights reserved. +# This file is a part of tebako +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +require_relative "patch_literals" +require_relative "patch_main" +require_relative "patch_libraries" +require_relative "patch_helpers" +require_relative "patch_buildsystem" + +# Tebako - an executable packager +module Tebako + module Packager + # Ruby patching definitions (pass2) + class Pass2Patch < Patch + def initialize(ostype, deps_lib_dir, ruby_ver) + super() + @ostype = ostype + @scmb = ScenarioManagerBase.new(@ostype) + @deps_lib_dir = deps_lib_dir + @ruby_ver = ruby_ver + end + + def patch_map + patch_map = patch_map_base + patch_map.store("thread_pthread.c", LINUX_MUSL_THREAD_PTHREAD_PATCH) if @scmb.musl? + if @scmb.msys? + patch_map.merge!(msys_patches) + elsif @ruby_ver.ruby3x? + patch_map.store("common.mk", COMMON_MK_PATCH) + end + extend_patch_map_r33(patch_map) + patch_map.store("prism_compile.c", PRISM_PATCHES) if @ruby_ver.ruby34? + patch_map + end + + private + + include Tebako::Packager::PatchBuildsystem + include Tebako::Packager::PatchLiterals + def extend_patch_map_r33(patch_map) + if @ruby_ver.ruby33? || @scmb.msys? + patch_map.store("config.status", + get_config_status_patch(@ostype, @deps_lib_dir, @ruby_ver)) + end + patch_map + end + + def dir_c_patch + pattern = ScenarioManagerBase.new.msys? ? "/* define system APIs */" : "#ifdef HAVE_GETATTRLIST" + patch = PatchHelpers.patch_c_file_pre(pattern) + patch.merge!(DIR_C_BASE_PATCH) + patch + end + + def dln_c_patch + pattern = "#ifndef dln_loaderror" + # Not using substitutions of dlxxx functions on Windows + patch = { + pattern => "#{@scmb.msys? ? C_FILE_SUBST_LESS : C_FILE_SUBST}\n#{pattern}\n" + } + + if @scmb.msys? + patch.merge!(@ruby_ver.ruby32? ? DLN_C_MSYS_PATCH : DLN_C_MSYS_PATCH_PRE32) + end + + patch + end + + def io_c_msys_patch + patch = @ruby_ver.ruby32? ? IO_C_MSYS_PATCH : IO_C_MSYS_PATCH_PRE_32 + patch.merge(IO_C_MSYS_BASE_PATCH) + end + + def io_c_patch + patch = PatchHelpers.patch_c_file_pre("/* define system APIs */") + patch.merge!(io_c_msys_patch) if @scmb.msys? + patch + end + + def util_c_patch + if @ruby_ver.ruby31? + PatchHelpers.patch_c_file_post("#endif /* !HAVE_GNU_QSORT_R */") + else + PatchHelpers.patch_c_file_pre("#ifndef S_ISDIR") + end + end + + def tool_mkconfig_rb_patch + subst = @scmb.msys? ? TOOL_MKCONFIG_RB_SUBST_MSYS : TOOL_MKCONFIG_RB_SUBST + { + " if fast[name]" => subst + } + end + + def msys_patches + { + "cygwin/GNUmakefile.in" => get_gnumakefile_in_patch_p2(@ruby_ver), + "ruby.c" => RUBY_C_MSYS_PATCHES, + "win32/file.c" => WIN32_FILE_C_MSYS_PATCHES, + "win32/win32.c" => WIN32_WIN32_C_MSYS_PATCHES + } + end + + def patch_map_base + { + "template/Makefile.in" => template_makefile_in_patch, + "tool/mkconfig.rb" => tool_mkconfig_rb_patch, + "dir.c" => dir_c_patch, "dln.c" => dln_c_patch, + "io.c" => io_c_patch, "main.c" => PatchMain.get_main_c_patch(@ruby_ver), + "file.c" => PatchHelpers.patch_c_file_pre("/* define system APIs */"), + "util.c" => util_c_patch + } + end + + def mlibs_subst + yjit_libs = @ruby_ver.ruby32only? ? "$(YJIT_LIBS) " : "" + { + "MAINLIBS = #{yjit_libs}@MAINLIBS@" => + "# -- Start of tebako patch -- \n" \ + "MAINLIBS = #{yjit_libs}#{PatchLibraries.mlibs(@ostype, @deps_lib_dir, @ruby_ver, true)}\n" \ + "# -- End of tebako patch -- \n" + } + end + + def template_makefile_in_patch + template_makefile_in_patch_two(@ruby_ver).merge(mlibs_subst) + end + end + end +end diff --git a/lib/tebako/packager/patch_buildsystem.rb b/lib/tebako/packager/patch_buildsystem.rb index bb228554..7f52645b 100644 --- a/lib/tebako/packager/patch_buildsystem.rb +++ b/lib/tebako/packager/patch_buildsystem.rb @@ -77,8 +77,8 @@ module PatchBuildsystem "$(EXTOBJS) $(LIBRUBYARG_STATIC) $(OUTFLAG)$@\n" \ "# -- End of tebako patch --" - def template_makefile_in_subst(ostype, ruby_ver) - if PatchHelpers.msys?(ostype) + def template_makefile_in_subst(ruby_ver) + if ScenarioManagerBase.new.msys? TEMPLATE_MAKEFILE_IN_BASE_PATCH_MSYS elsif !ruby_ver.ruby31? TEMPLATE_MAKEFILE_IN_BASE_PATCH_PRE_3_1 @@ -87,16 +87,20 @@ def template_makefile_in_subst(ostype, ruby_ver) end end - def template_makefile_in_patch_two(ostype, ruby_ver) + def template_makefile_in_patch_two(ruby_ver) if !ruby_ver.ruby31? - { TEMPLATE_MAKEFILE_IN_BASE_PATTERN_PRE_3_1 => template_makefile_in_subst(ostype, ruby_ver) } + { TEMPLATE_MAKEFILE_IN_BASE_PATTERN_PRE_3_1 => template_makefile_in_subst(ruby_ver) } elsif !ruby_ver.ruby33? - { TEMPLATE_MAKEFILE_IN_BASE_PATTERN_PRE_3_3 => template_makefile_in_subst(ostype, ruby_ver) } + { TEMPLATE_MAKEFILE_IN_BASE_PATTERN_PRE_3_3 => template_makefile_in_subst(ruby_ver) } else - { TEMPLATE_MAKEFILE_IN_BASE_PATTERN => template_makefile_in_subst(ostype, ruby_ver) } + { TEMPLATE_MAKEFILE_IN_BASE_PATTERN => template_makefile_in_subst(ruby_ver) } end end + GNUMAKEFILE_IN_DLLTOOL_SUBST = <<~SUBST + $(Q) dlltool --output-lib=$(LIBRUBY) --output-def=tebako.def --export-all $(LIBRUBY_A) --output-exp=$(RUBY_EXP) # tebako patched + SUBST + # This MSYS specific thing ensure compilation of winmain.c # Did try to understand why it did not work out of the box GNUMAKEFILE_IN_WINMAIN_SUBST = <<~SUBST diff --git a/lib/tebako/packager/patch_helpers.rb b/lib/tebako/packager/patch_helpers.rb index 081ecf79..9e92a209 100755 --- a/lib/tebako/packager/patch_helpers.rb +++ b/lib/tebako/packager/patch_helpers.rb @@ -62,18 +62,6 @@ def get_prefix_linux(package) out end - def exe_suffix(ostype) - msys?(ostype) ? ".exe" : "" - end - - def msys?(ostype) - ostype =~ /msys|cygwin|mingw/ - end - - def macos?(ostype) - ostype =~ /darwin/ - end - def patch_c_file_pre(pattern) { pattern => "#{PatchLiterals::C_FILE_SUBST}\n#{pattern}" diff --git a/lib/tebako/ruby_builder.rb b/lib/tebako/ruby_builder.rb index abc5b456..070f36a5 100644 --- a/lib/tebako/ruby_builder.rb +++ b/lib/tebako/ruby_builder.rb @@ -37,7 +37,7 @@ class RubyBuilder def initialize(ruby_ver, src_dir) @ruby_ver = ruby_ver @src_dir = src_dir - @ncores = BuildHelpers.ncores + @ncores = ScenarioManagerBase.new.ncores end # Final build of tebako package diff --git a/lib/tebako/ruby_version.rb b/lib/tebako/ruby_version.rb index 4f43fedd..8808b369 100755 --- a/lib/tebako/ruby_version.rb +++ b/lib/tebako/ruby_version.rb @@ -120,8 +120,7 @@ def version_check_format end def version_check_msys - if Gem::Version.new(@ruby_version) < Gem::Version.new(MIN_RUBY_VERSION_WINDOWS) && - RUBY_PLATFORM =~ /msys|mingw|cygwin/ + if Gem::Version.new(@ruby_version) < Gem::Version.new(MIN_RUBY_VERSION_WINDOWS) && ScenarioManagerBase.new.msys? raise Tebako::Error.new("Ruby version #{@ruby_version} is not supported on Windows", 111) end end diff --git a/lib/tebako/scenario_manager.rb b/lib/tebako/scenario_manager.rb index 89c597ac..83117e53 100644 --- a/lib/tebako/scenario_manager.rb +++ b/lib/tebako/scenario_manager.rb @@ -38,47 +38,93 @@ module Tebako BUNDLER_VERSION = "2.4.22" RUBYGEMS_VERSION = "3.4.22" - # Manages packaging scenario based on input files (gemfile, gemspec, etc) - class ScenarioManager - def initialize(fs_root, fs_entrance) - @with_gemfile = @with_lockfile = @needs_bundler = false - @bundler_version = BUNDLER_VERSION - initialize_root(fs_root) - initialize_entry_point(fs_entrance || "stub.rb") + # A couple of static Scenario definitions + class ScenarioManagerBase + def initialize(ostype = RUBY_PLATFORM) + @ostype = ostype + @linux = @ostype =~ /linux/ ? true : false + @musl = @ostype =~ /linux-musl/ ? true : false + @macos = @ostype =~ /darwin/ ? true : false + @msys = @ostype =~ /msys|mingw|cygwin/ ? true : false + + @fs_mount_point = @msys ? "A:/__tebako_memfs__" : "/__tebako_memfs__" + @exe_suffix = @msys ? ".exe" : "" end - attr_reader :fs_entry_point, :fs_mount_point, :fs_entrance, :gemfile_path, :needs_bundler, :with_gemfile + attr_reader :fs_mount_point, :exe_suffix - def bundler_reference - @needs_bundler ? "_#{@bundler_version}_" : nil + def b_env + u_flags = if @macos + "-DTARGET_OS_SIMULATOR=0 -DTARGET_OS_IPHONE=0 #{ENV.fetch("CXXFLAGS", nil)}" + else + ENV.fetch("CXXFLAGS", nil) + end + @b_env ||= { "CXXFLAGS" => u_flags } end - def configure_scenario - @fs_mount_point = if msys? - "A:/__tebako_memfs__" - else - "/__tebako_memfs__" - end - - lookup_files - configure_scenario_inner + def linux? + @linux end - def exe_suffix - @exe_suffix ||= msys? ? ".exe" : "" + def m_files + # [TODO] + # Ninja generates incorrect script for tebako press target -- gets lost in a chain custom targets + # Using makefiles has negative performance impact so it needs to be fixed + + @m_files ||= if @linux || @macos + "Unix Makefiles" + elsif @msys + "MinGW Makefiles" + else + raise Tebako::Error.new("#{RUBY_PLATFORM} is not supported.", 112) + end end def macos? - @macos ||= RUBY_PLATFORM =~ /darwin/ ? true : false + @macos end def msys? - @msys ||= RUBY_PLATFORM =~ /msys|mingw|cygwin/ ? true : false + @msys end - private + def musl? + @musl + end + + def ncores + if @ncores.nil? + if @macos + out, st = Open3.capture2e("sysctl", "-n", "hw.ncpu") + else + out, st = Open3.capture2e("nproc", "--all") + end + + @ncores = !st.signaled? && st.exitstatus.zero? ? out.strip.to_i : 4 + end + @ncores + end + end + + # Manages packaging scenario based on input files (gemfile, gemspec, etc) + class ScenarioManager < ScenarioManagerBase + def initialize(fs_root, fs_entrance) + super() + @with_gemfile = @with_lockfile = @needs_bundler = false + @bundler_version = BUNDLER_VERSION + initialize_root(fs_root) + initialize_entry_point(fs_entrance || "stub.rb") + end + + attr_reader :fs_entry_point, :fs_entrance, :gemfile_path, :needs_bundler, :with_gemfile + + def bundler_reference + @needs_bundler ? "_#{@bundler_version}_" : nil + end + + def configure_scenario + lookup_files - def configure_scenario_inner case @gs_length when 0 configure_scenario_no_gemspec @@ -89,6 +135,8 @@ def configure_scenario_inner end end + private + def configure_scenario_no_gemspec @fs_entry_point = "/local/#{@fs_entrance}" if @with_gemfile || @g_length.zero? diff --git a/spec/build_helpers_spec.rb b/spec/build_helpers_spec.rb index 22ad863c..40eec602 100644 --- a/spec/build_helpers_spec.rb +++ b/spec/build_helpers_spec.rb @@ -31,55 +31,7 @@ # rubocop:disable Metrics/BlockLength RSpec.describe Tebako::BuildHelpers do - describe ".ncores" do - context "when on macOS" do - before do - stub_const("RUBY_PLATFORM", "darwin") - status_double = double(exitstatus: 0, signaled?: false) - allow(Open3).to receive(:capture2e).with("sysctl", "-n", "hw.ncpu").and_return(["4", status_double]) - end - - it "returns the number of cores" do - expect(described_class.ncores).to eq(4) - end - end - - context "when on Linux" do - before do - stub_const("RUBY_PLATFORM", "linux") - status_double = double(exitstatus: 0, signaled?: false) - allow(Open3).to receive(:capture2e).with("nproc", "--all").and_return(["8", status_double]) - end - - it "returns the number of cores" do - expect(described_class.ncores).to eq(8) - end - end - - context "when the command fails" do - before do - status_double = double(exitstatus: 1, signaled?: false) - allow(Open3).to receive(:capture2e).and_return(["", status_double]) - end - - it "returns 4 as a default value" do - expect(described_class.ncores).to eq(4) - end - end - - context "when the command is terminated by a signal" do - before do - status_double = double(exitstatus: nil, signaled?: true, termsig: 9) - allow(Open3).to receive(:capture2e).and_return(["", status_double]) - end - - it "returns 4 as a default value" do - expect(described_class.ncores).to eq(4) - end - end - end - - describe ".run_with_capture" do + describe "#run_with_capture" do let(:args) { %w[echo hello] } describe ".run_with_capture" do diff --git a/spec/deploy_helper_spec.rb b/spec/deploy_helper_spec.rb index 097677a3..d31432e5 100644 --- a/spec/deploy_helper_spec.rb +++ b/spec/deploy_helper_spec.rb @@ -52,7 +52,6 @@ expect(deploy_helper.instance_variable_get(:@target_dir)).to eq(target_dir) expect(deploy_helper.instance_variable_get(:@pre_dir)).to eq(pre_dir) expect(deploy_helper.instance_variable_get(:@verbose)).to eq(false) - expect(deploy_helper.instance_variable_get(:@ncores)).to be_a(Integer) end end @@ -180,8 +179,7 @@ before do allow(Tebako::BuildHelpers).to receive(:ncores).and_return(1) if RUBY_PLATFORM =~ /darwin/ stub_const("RUBY_PLATFORM", "linux") - allow(deploy_helper).to receive(:lookup_files) - allow(deploy_helper).to receive(:configure_scenario_inner) + allow(deploy_helper).to receive(:configure_scenario) deploy_helper.configure(ruby_ver, cwd) end @@ -338,8 +336,7 @@ before do allow(Tebako::BuildHelpers).to receive(:ncores).and_return(1) if RUBY_PLATFORM =~ /darwin/ stub_const("RUBY_PLATFORM", "linux") - allow(deploy_helper).to receive(:lookup_files) - allow(deploy_helper).to receive(:configure_scenario_inner) + allow(deploy_helper).to receive(:configure_scenario) deploy_helper.configure(ruby_ver, cwd) end @@ -521,7 +518,7 @@ expect(deploy_helper).to receive(:bundle_config).ordered expect(Tebako::BuildHelpers).to receive(:run_with_capture_v) .with([deploy_helper.instance_variable_get(:@bundler_command), nil, "install", - "--jobs=#{deploy_helper.instance_variable_get(:@ncores)}"]) + "--jobs=#{deploy_helper.ncores}"]) .ordered expect(deploy_helper).to receive(:check_entry_point).with("local").ordered @@ -765,4 +762,5 @@ end end end + # rubocop:enable Metrics/BlockLength diff --git a/spec/options_manager_spec.rb b/spec/options_manager_spec.rb index 50ca273c..d5f71e9a 100644 --- a/spec/options_manager_spec.rb +++ b/spec/options_manager_spec.rb @@ -37,48 +37,6 @@ let(:ruby_ver) { "3.2.6" } let(:ruby_hash) { Tebako::RubyVersion::RUBY_VERSIONS["3.2.6"] } - describe "#b_env" do - let(:options_manager) { Tebako::OptionsManager.new({}) } - before do - @original_host_os = RbConfig::CONFIG["host_os"] - @original_cxxflags = ENV.fetch("CXXFLAGS", nil) - end - - after do - RbConfig::CONFIG["host_os"] = @original_host_os - ENV["CXXFLAGS"] = @original_cxxflags - end - - context "when host OS is Darwin" do - it "sets CXXFLAGS with TARGET_OS_SIMULATOR and TARGET_OS_IPHONE" do - RbConfig::CONFIG["host_os"] = "darwin" - ENV["CXXFLAGS"] = "-O2" - - expected_flags = "-DTARGET_OS_SIMULATOR=0 -DTARGET_OS_IPHONE=0 -O2" - expect(options_manager.b_env["CXXFLAGS"]).to eq(expected_flags) - end - end - - context "when host OS is not Darwin" do - it "sets CXXFLAGS with the value from ENV" do - RbConfig::CONFIG["host_os"] = "linux" - ENV["CXXFLAGS"] = "-O2" - - expected_flags = "-O2" - expect(options_manager.b_env["CXXFLAGS"]).to eq(expected_flags) - end - end - - context "when CXXFLAGS is not set in ENV" do - it "sets CXXFLAGS to nil" do - RbConfig::CONFIG["host_os"] = "linux" - ENV.delete("CXXFLAGS") - - expect(options_manager.b_env["CXXFLAGS"]).to be_nil - end - end - end - describe "#cfg_options" do let(:deps) { File.join(Dir.pwd, "deps") } let(:output_folder) { File.join(Dir.pwd, "o") } @@ -250,50 +208,6 @@ end end - describe "#m_files" do - let(:options) { {} } - let(:options_manager) { Tebako::OptionsManager.new(options) } - context "when on a Linux platform" do - before do - stub_const("RUBY_PLATFORM", "linux") - end - - it 'returns "Unix Makefiles"' do - expect(options_manager.m_files).to eq("Unix Makefiles") - end - end - - context "when on a macOS platform" do - before do - stub_const("RUBY_PLATFORM", "darwin") - end - - it 'returns "Unix Makefiles"' do - expect(options_manager.m_files).to eq("Unix Makefiles") - end - end - - context "when on a Windows platform" do - before do - stub_const("RUBY_PLATFORM", "msys") - end - - it 'returns "MinGW Makefiles"' do - expect(options_manager.m_files).to eq("MinGW Makefiles") - end - end - - context "when on an unsupported platform" do - before do - stub_const("RUBY_PLATFORM", "unsupported") - end - - it "raises a Tebako::Error" do - expect { options_manager.m_files }.to raise_error(Tebako::Error, "unsupported is not supported.") - end - end - end - describe "#package" do let(:options_manager) { OptionsManager.new(options) } diff --git a/spec/packager/pass2_patch_spec.rb b/spec/packager/pass2_patch_spec.rb new file mode 100644 index 00000000..68817ea0 --- /dev/null +++ b/spec/packager/pass2_patch_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +# Copyright (c) 2025 [Ribose Inc](https://www.ribose.com). +# All rights reserved. +# This file is a part of tebako +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# rubocop:disable Metrics/BlockLength +RSpec.describe Tebako::Packager::Pass2Patch do + let(:ostype) { "linux-gnu" } + let(:deps_lib_dir) { "deps/lib" } + let(:ruby_ver) do + double("RubyVersion", ruby34?: false, ruby3x?: true, ruby32only?: true, ruby33?: false, ruby32?: false, + ruby31?: false) + end + let(:patch) { described_class.new(ostype, deps_lib_dir, ruby_ver) } + let(:scmb) { instance_double(Tebako::ScenarioManagerBase, msys?: false, musl?: false) } + + before do + allow(Tebako::ScenarioManagerBase).to receive(:new).and_return(scmb) + end + + describe "#initialize" do + it "sets instance variables correctly" do + expect(patch.instance_variable_get(:@ostype)).to eq(ostype) + expect(patch.instance_variable_get(:@deps_lib_dir)).to eq(deps_lib_dir) + expect(patch.instance_variable_get(:@ruby_ver)).to eq(ruby_ver) + end + end + + describe "#patch_map" do + context "when on musl platform" do + before { allow(scmb).to receive(:musl?).and_return(true) } + + it "includes LINUX_MUSL_THREAD_PTHREAD_PATCH" do + expect(patch.patch_map).to include("thread_pthread.c" => described_class::LINUX_MUSL_THREAD_PTHREAD_PATCH) + end + end + + context "when on msys platform" do + before do + allow(scmb).to receive(:msys?).and_return(true) + allow(patch).to receive(:msys_patches).and_return({ "msys_file" => "msys_patch" }) + end + + it "includes msys patches" do + expect(patch.patch_map).to include("msys_file" => "msys_patch") + end + end + + context "when ruby version is 3.x" do + before { allow(ruby_ver).to receive(:ruby3x?).and_return(true) } + + it "includes COMMON_MK_PATCH when not on msys" do + expect(patch.patch_map).to include("common.mk" => described_class::COMMON_MK_PATCH) + end + end + + context "when ruby version is 3.3" do + before do + allow(ruby_ver).to receive(:ruby33?).and_return(true) + allow(patch).to receive(:get_config_status_patch).and_return("config_status_patch") + end + + it "includes config.status patch" do + expect(patch.patch_map).to include("config.status" => "config_status_patch") + end + end + + context "when ruby version is 3.4" do + before { allow(ruby_ver).to receive(:ruby34?).and_return(true) } + + it "includes PRISM_PATCHES" do + expect(patch.patch_map).to include("prism_compile.c" => described_class::PRISM_PATCHES) + end + end + end + + describe "#patch_map_base" do + it "includes all base patches" do + base_patches = patch.send(:patch_map_base) + expect(base_patches.keys).to include( + "template/Makefile.in", + "tool/mkconfig.rb", + "dir.c", + "dln.c", + "io.c", + "main.c", + "file.c", + "util.c" + ) + end + end + + describe "#dir_c_patch" do + context "when on msys" do + let(:ostype) { "msys" } + before { allow(scmb).to receive(:msys?).and_return(true) } + + it "uses correct pattern for msys" do + patch = described_class.new(ostype, deps_lib_dir, ruby_ver) + expect(patch.send(:dir_c_patch)).to include("/* define system APIs */" => anything) + end + end + + context "when not on msys" do + before { allow(scmb).to receive(:msys?).and_return(false) } + + it "uses correct pattern for non-msys" do + patch = described_class.new(ostype, deps_lib_dir, ruby_ver) + expect(patch.send(:dir_c_patch)).to include("#ifdef HAVE_GETATTRLIST" => anything) + end + end + end + + describe "#msys_patches" do + let(:expected_gnumakefile_patch) { "gnumakefile_patch_content" } + + before do + allow(patch).to receive(:get_gnumakefile_in_patch_p2) + .with(ruby_ver) + .and_return(expected_gnumakefile_patch) + end + + it "returns correct patches for MSys platform" do + expected_patches = { + "cygwin/GNUmakefile.in" => expected_gnumakefile_patch, + "ruby.c" => described_class::RUBY_C_MSYS_PATCHES, + "win32/file.c" => described_class::WIN32_FILE_C_MSYS_PATCHES, + "win32/win32.c" => described_class::WIN32_WIN32_C_MSYS_PATCHES + } + + expect(patch.send(:msys_patches)).to eq(expected_patches) + end + + it "calls get_gnumakefile_in_patch_p2 with correct ruby_ver" do + expect(patch).to receive(:get_gnumakefile_in_patch_p2).with(ruby_ver) + patch.send(:msys_patches) + end + end + + describe "#util_c_patch" do + context "when ruby version is 3.1" do + before do + allow(ruby_ver).to receive(:ruby31?).and_return(true) + end + + it "uses post-pattern for ruby 3.1" do + patch = described_class.new(ostype, deps_lib_dir, ruby_ver) + expect(Tebako::Packager::PatchHelpers).to receive(:patch_c_file_post) + .with("#endif /* !HAVE_GNU_QSORT_R */") + patch.send(:util_c_patch) + end + end + + context "when ruby version is not 3.1" do + before do + allow(ruby_ver).to receive(:ruby31?).and_return(false) + end + + it "uses pre-pattern for non-ruby 3.1" do + patch = described_class.new(ostype, deps_lib_dir, ruby_ver) + expect(Tebako::Packager::PatchHelpers).to receive(:patch_c_file_pre) + .with("#ifndef S_ISDIR") + patch.send(:util_c_patch) + end + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/packager/patch_helpers_spec.rb b/spec/packager/patch_helpers_spec.rb index c5319da4..82fb6cc3 100644 --- a/spec/packager/patch_helpers_spec.rb +++ b/spec/packager/patch_helpers_spec.rb @@ -105,19 +105,6 @@ end end - describe "#exe_suffix" do - it "returns '.exe' for msys-based platforms" do - expect(Tebako::Packager::PatchHelpers.exe_suffix("msys2")).to eq(".exe") - expect(Tebako::Packager::PatchHelpers.exe_suffix("mingw32")).to eq(".exe") - expect(Tebako::Packager::PatchHelpers.exe_suffix("cygwin")).to eq(".exe") - end - - it "returns an empty string for non-msys platforms" do - expect(Tebako::Packager::PatchHelpers.exe_suffix("darwin")).to eq("") - expect(Tebako::Packager::PatchHelpers.exe_suffix("linux")).to eq("") - end - end - describe "#restore_and_save_files" do # rubocop:disable Metrics/BlockLength let(:ruby_source_dir) { File.join(temp_dir, "ruby_source") } let(:test_files) { ["test1.rb", "test2.rb"] } diff --git a/spec/packager_spec.rb b/spec/packager_spec.rb index 2205e3a1..38f0f633 100644 --- a/spec/packager_spec.rb +++ b/spec/packager_spec.rb @@ -96,7 +96,6 @@ end describe "#finalize" do - let(:os_type) { "linux" } let(:src_dir) { "/path/to/src" } let(:app_name) { "my_app" } let(:ruby_ver) { "2.7.2" } @@ -106,32 +105,32 @@ before do allow(Tebako::RubyBuilder).to receive(:new).and_return(ruby_builder) allow(ruby_builder).to receive(:target_build) - allow(Tebako::Packager::PatchHelpers).to receive(:exe_suffix).and_return("") + allow_any_instance_of(Tebako::ScenarioManagerBase).to receive(:exe_suffix).and_return("") allow(Tebako::Packager).to receive(:patchelf) - allow(Tebako::Packager).to receive(:strip_or_copy) + allow(Tebako::Stripper).to receive(:strip) end it "creates a new RubyBuilder with the correct parameters" do expect(Tebako::RubyBuilder).to receive(:new).with(ruby_ver, src_dir).and_return(ruby_builder) - Tebako::Packager.finalize(os_type, src_dir, app_name, ruby_ver, patchelf) + Tebako::Packager.finalize(src_dir, app_name, ruby_ver, patchelf) end it "calls target_build on the RubyBuilder" do expect(ruby_builder).to receive(:target_build) - Tebako::Packager.finalize(os_type, src_dir, app_name, ruby_ver, patchelf) + Tebako::Packager.finalize(src_dir, app_name, ruby_ver, patchelf) end it "calls patchelf with the correct parameters" do src_name = File.join(src_dir, "ruby") expect(Tebako::Packager).to receive(:patchelf).with(src_name, patchelf) - Tebako::Packager.finalize(os_type, src_dir, app_name, ruby_ver, patchelf) + Tebako::Packager.finalize(src_dir, app_name, ruby_ver, patchelf) end - it "calls strip_or_copy with the correct parameters" do + it "calls strip_file with the correct parameters" do src_name = File.join(src_dir, "ruby") package_name = app_name.to_s - expect(Tebako::Packager).to receive(:strip_or_copy).with(os_type, src_name, package_name) - Tebako::Packager.finalize(os_type, src_dir, app_name, ruby_ver, patchelf) + expect(Tebako::Stripper).to receive(:strip_file).with(src_name, package_name) + Tebako::Packager.finalize(src_dir, app_name, ruby_ver, patchelf) end end @@ -244,7 +243,7 @@ let(:patch_map) { { "file1" => "patch1", "file2" => "patch2" } } before do - allow(Tebako::Packager::Pass2).to receive(:get_patch_map).and_return(patch_map) + allow_any_instance_of(Tebako::Packager::Pass2Patch).to receive(:patch_map).and_return(patch_map) allow(Tebako::Packager).to receive(:do_patch) end @@ -310,34 +309,6 @@ end end end - - describe "#strip_or_copy" do - let(:os_type) { "linux" } - let(:src_name) { "binary" } - let(:package_name) { "package" } - - context "when running on MSys" do - before do - allow(Tebako::Packager::PatchHelpers).to receive(:msys?).with(os_type).and_return(true) - end - - it "copies the file" do - expect(FileUtils).to receive(:cp).with(src_name, package_name) - Tebako::Packager.send(:strip_or_copy, os_type, src_name, package_name) - end - end - - context "when not running on MSys" do - before do - allow(Tebako::Packager::PatchHelpers).to receive(:msys?).with(os_type).and_return(false) - end - - it "strips the file" do - expect(Tebako::Stripper).to receive(:strip_file).with(src_name, package_name) - Tebako::Packager.send(:strip_or_copy, os_type, src_name, package_name) - end - end - end end # rubocop:enable Metrics/BlockLength diff --git a/spec/ruby_builder_spec.rb b/spec/ruby_builder_spec.rb index cc494c95..bd796aa2 100644 --- a/spec/ruby_builder_spec.rb +++ b/spec/ruby_builder_spec.rb @@ -39,7 +39,7 @@ let(:builder) { described_class.new(Tebako::RubyVersion.new(ruby_ver), src_dir) } before do - allow(Tebako::BuildHelpers).to receive(:ncores).and_return(ncores) + allow_any_instance_of(Tebako::ScenarioManagerBase).to receive(:ncores).and_return(ncores) allow(Tebako::BuildHelpers).to receive(:run_with_capture) allow(Dir).to receive(:chdir).with(src_dir).and_yield end @@ -73,7 +73,7 @@ let(:builder) { described_class.new(Tebako::RubyVersion.new(ruby_ver), src_dir) } before do - allow(Tebako::BuildHelpers).to receive(:ncores).and_return(ncores) + allow_any_instance_of(Tebako::ScenarioManagerBase).to receive(:ncores).and_return(ncores) allow(Tebako::BuildHelpers).to receive(:run_with_capture) allow(Dir).to receive(:chdir).with(src_dir).and_yield end diff --git a/spec/scenario_manager_base_spec.rb b/spec/scenario_manager_base_spec.rb new file mode 100644 index 00000000..9285f8c4 --- /dev/null +++ b/spec/scenario_manager_base_spec.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +# Copyright (c) 2024-2025 [Ribose Inc](https://www.ribose.com). +# All rights reserved. +# This file is a part of tebako +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +require "tmpdir" +require_relative "../lib/tebako/scenario_manager" + +# rubocop:disable Metrics/BlockLength +RSpec.describe Tebako::ScenarioManagerBase do + describe "#initialize" do + context "on msys platform" do + before do + stub_const("RUBY_PLATFORM", "msys") + end + + it "sets correct fs_mount_point and exe_suffix" do + manager = described_class.new + expect(manager.fs_mount_point).to eq("A:/__tebako_memfs__") + expect(manager.exe_suffix).to eq(".exe") + end + end + + context "on non-msys platform" do + before do + stub_const("RUBY_PLATFORM", "linux") + end + + it "sets correct fs_mount_point and exe_suffix" do + manager = described_class.new + expect(manager.fs_mount_point).to eq("/__tebako_memfs__") + expect(manager.exe_suffix).to eq("") + end + end + end + + describe "#b_env" do + before do + @original_cxxflags = ENV.fetch("CXXFLAGS", nil) + end + + after do + ENV["CXXFLAGS"] = @original_cxxflags + end + + context "when host OS is Darwin" do + it "sets CXXFLAGS with TARGET_OS_SIMULATOR and TARGET_OS_IPHONE" do + stub_const("RUBY_PLATFORM", "darwin") + ENV["CXXFLAGS"] = "-O2" + + expected_flags = "-DTARGET_OS_SIMULATOR=0 -DTARGET_OS_IPHONE=0 -O2" + expect(described_class.new.b_env["CXXFLAGS"]).to eq(expected_flags) + end + end + + context "when host OS is not Darwin" do + it "sets CXXFLAGS with the value from ENV" do + stub_const("RUBY_PLATFORM", "linux") + ENV["CXXFLAGS"] = "-O2" + + expected_flags = "-O2" + expect(described_class.new.b_env["CXXFLAGS"]).to eq(expected_flags) + end + end + + context "when CXXFLAGS is not set in ENV" do + it "sets CXXFLAGS to nil" do + stub_const("RUBY_PLATFORM", "linux") + ENV.delete("CXXFLAGS") + + expect(described_class.new.b_env["CXXFLAGS"]).to be_nil + end + end + end + + describe "#linux?" do + context "on linux platform" do + before do + stub_const("RUBY_PLATFORM", "linux") + end + + it "returns true" do + expect(described_class.new.linux?).to be true + end + end + + context "on non-linux platform" do + before do + stub_const("RUBY_PLATFORM", "darwin") + end + + it "returns false" do + expect(described_class.new.linux?).to be false + end + end + end + + describe "#musl?" do + context "on linux-musl platform" do + before do + stub_const("RUBY_PLATFORM", "linux-musl") + end + + it "returns true" do + expect(described_class.new.musl?).to be true + end + end + + context "on non-linux platform" do + before do + stub_const("RUBY_PLATFORM", "darwin") + end + + it "returns false" do + expect(described_class.new.musl?).to be false + end + end + end + + describe "#m_files" do + context "when on a Linux platform" do + before do + stub_const("RUBY_PLATFORM", "linux") + end + + it 'returns "Unix Makefiles"' do + expect(described_class.new.m_files).to eq("Unix Makefiles") + end + end + + context "when on a macOS platform" do + before do + stub_const("RUBY_PLATFORM", "darwin") + end + + it 'returns "Unix Makefiles"' do + expect(described_class.new.m_files).to eq("Unix Makefiles") + end + end + + context "when on a Windows platform" do + before do + stub_const("RUBY_PLATFORM", "msys") + end + + it 'returns "MinGW Makefiles"' do + expect(described_class.new.m_files).to eq("MinGW Makefiles") + end + end + + context "when on an unsupported platform" do + before do + stub_const("RUBY_PLATFORM", "unsupported") + end + + it "raises a Tebako::Error" do + expect { described_class.new.m_files }.to raise_error(Tebako::Error, "unsupported is not supported.") + end + end + end + + describe "#macos?" do + context "on macos platform" do + before do + stub_const("RUBY_PLATFORM", "darwin") + end + + it "returns true" do + expect(described_class.new.macos?).to be true + end + end + + context "on non-macos platform" do + before do + stub_const("RUBY_PLATFORM", "linux") + end + + it "returns false" do + expect(described_class.new.macos?).to be false + end + end + end + + describe "#msys?" do + context "on msys platform" do + before do + stub_const("RUBY_PLATFORM", "msys") + end + + it "returns true" do + expect(described_class.new.msys?).to be true + end + end + + context "on non-msys platform" do + before do + stub_const("RUBY_PLATFORM", "darwin") + end + + it "returns false" do + expect(described_class.new.msys?).to be false + end + end + end + + describe "#ncores" do + context "when on macOS" do + before do + stub_const("RUBY_PLATFORM", "darwin") + status_double = double(exitstatus: 0, signaled?: false) + allow(Open3).to receive(:capture2e).with("sysctl", "-n", "hw.ncpu").and_return(["4", status_double]) + end + + it "returns the number of cores" do + expect(described_class.new.ncores).to eq(4) + end + end + + context "when on Linux" do + before do + stub_const("RUBY_PLATFORM", "linux") + status_double = double(exitstatus: 0, signaled?: false) + allow(Open3).to receive(:capture2e).with("nproc", "--all").and_return(["8", status_double]) + end + + it "returns the number of cores" do + expect(described_class.new.ncores).to eq(8) + end + end + + context "when the command fails" do + before do + status_double = double(exitstatus: 1, signaled?: false) + allow(Open3).to receive(:capture2e).and_return(["", status_double]) + end + + it "returns 4 as a default value" do + expect(described_class.new.ncores).to eq(4) + end + end + + context "when the command is terminated by a signal" do + before do + status_double = double(exitstatus: nil, signaled?: true, termsig: 9) + allow(Open3).to receive(:capture2e).and_return(["", status_double]) + end + + it "returns 4 as a default value" do + expect(described_class.new.ncores).to eq(4) + end + end + end +end + +# rubocop:enable Metrics/BlockLength diff --git a/spec/scenario_manager_spec.rb b/spec/scenario_manager_spec.rb index 34e3d839..c1d9fb02 100644 --- a/spec/scenario_manager_spec.rb +++ b/spec/scenario_manager_spec.rb @@ -52,7 +52,6 @@ it "sets instance variables correctly" do expect(scenario_manager.instance_variable_get(:@fs_root)).to eq(fs_root) expect(scenario_manager.instance_variable_get(:@fs_entrance)).to eq(fs_entrance) - expect(scenario_manager.instance_variable_get(:@fs_mount_point)).to eq("/__tebako_memfs__") end end @@ -65,73 +64,76 @@ it "sets instance variables correctly" do expect(scenario_manager.instance_variable_get(:@fs_root)).to eq(fs_root) expect(scenario_manager.instance_variable_get(:@fs_entrance)).to eq(fs_entrance) - expect(scenario_manager.instance_variable_get(:@fs_mount_point)).to eq("A:/__tebako_memfs__") end end end - describe "#configure_scenario_inner" do + describe "#configure_scenario" do before do allow(scenario_manager).to receive(:lookup_files) - allow(scenario_manager).to receive(:configure_scenario_inner) - scenario_manager.configure_scenario end - it "calls configure_scenario_inner" do - expect(scenario_manager).to have_received(:configure_scenario_inner) - end - end - - describe "#configure_scenario" do - context "when @gs_length is 0" do + context "when no gemspecs are present" do before do scenario_manager.instance_variable_set(:@gs_length, 0) + scenario_manager.instance_variable_set(:@g_length, 0) + end + + it "sets scenario to :simple_script" do + scenario_manager.configure_scenario + expect(scenario_manager.instance_variable_get(:@scenario)).to eq(:simple_script) end - it "calls configure_scenario_no_gemspec" do - expect(scenario_manager).to receive(:configure_scenario_no_gemspec) - scenario_manager.send(:configure_scenario_inner) + context "with Gemfile" do + before do + scenario_manager.instance_variable_set(:@with_gemfile, true) + end + + it "sets scenario to :gemfile" do + scenario_manager.configure_scenario + expect(scenario_manager.instance_variable_get(:@scenario)).to eq(:gemfile) + end end end - context "when @gs_length is 1" do + context "when one gemspec is present" do before do scenario_manager.instance_variable_set(:@gs_length, 1) end - context "and @with_gemfile is true" do + context "without Gemfile" do before do - scenario_manager.instance_variable_set(:@with_gemfile, true) - scenario_manager.send(:configure_scenario_inner) + scenario_manager.instance_variable_set(:@with_gemfile, false) end - it "sets @scenario to :gemspec_and_gemfile" do - expect(scenario_manager.instance_variable_get(:@scenario)).to eq(:gemspec_and_gemfile) + it "sets scenario to :gemspec" do + scenario_manager.configure_scenario + expect(scenario_manager.instance_variable_get(:@scenario)).to eq(:gemspec) end end - context "and @with_gemfile is false" do + context "with Gemfile" do before do - scenario_manager.instance_variable_set(:@with_gemfile, false) - scenario_manager.send(:configure_scenario_inner) + scenario_manager.instance_variable_set(:@with_gemfile, true) end - it "sets @scenario to :gemspec" do - expect(scenario_manager.instance_variable_get(:@scenario)).to eq(:gemspec) + it "sets scenario to :gemspec_and_gemfile" do + scenario_manager.configure_scenario + expect(scenario_manager.instance_variable_get(:@scenario)).to eq(:gemspec_and_gemfile) end end end - context "when @gs_length is greater than 1" do + context "when multiple gemspecs are present" do before do scenario_manager.instance_variable_set(:@gs_length, 2) end - it "raises a Tebako::Error" do - expect do - scenario_manager.send(:configure_scenario_inner) - end.to raise_error(Tebako::Error, - "Multiple Ruby gemspecs found in #{scenario_manager.instance_variable_get(:@fs_root)}") + it "raises error" do + expect { scenario_manager.configure_scenario }.to raise_error( + Tebako::Error, + "Multiple Ruby gemspecs found in #{fs_root}" + ) end end end @@ -174,28 +176,6 @@ end end - describe "#exe_suffix" do - context "when running on Windows" do - before do - stub_const("RUBY_PLATFORM", "msys") - end - - it "returns .exe" do - expect(scenario_manager.exe_suffix).to eq(".exe") - end - end - - context "when not running on Windows" do - before do - stub_const("RUBY_PLATFORM", "linux") - end - - it "returns an empty string" do - expect(scenario_manager.exe_suffix).to eq("") - end - end - end - describe "#lookup_files" do let(:tmp_root) { Dir.mktmpdir } let(:scenario_manager) { described_class.new(tmp_root, "dummy_entry.rb") } diff --git a/tebako.gemspec b/tebako.gemspec index 0184985c..b5e525f1 100644 --- a/tebako.gemspec +++ b/tebako.gemspec @@ -63,8 +63,10 @@ Gem::Specification.new do |spec| spec.add_dependency "thor", "~> 1.2" spec.add_dependency "yaml", "~> 0.2.1" + spec.add_development_dependency "debug" spec.add_development_dependency "hoe" spec.add_development_dependency "minitest" + spec.add_development_dependency "rdbg" spec.add_development_dependency "rspec", "~> 3.2" spec.add_development_dependency "rubocop", "~> 1.52" spec.add_development_dependency "rubocop-rubycw"