1# -*- ruby -*-
2require 'rake/extensiontask'
3require 'rspec/core/rake_task'
4require 'rubocop/rake_task'
5require 'bundler/gem_tasks'
6require 'fileutils'
7
8require_relative 'build_config.rb'
9
10load 'tools/distrib/rake_compiler_docker_image.rb'
11
12# Add rubocop style checking tasks
13RuboCop::RakeTask.new(:rubocop) do |task|
14  task.options = ['-c', 'src/ruby/.rubocop.yml']
15  # add end2end tests to formatter but don't add generated proto _pb.rb's
16  task.patterns = ['src/ruby/{lib,spec}/**/*.rb', 'src/ruby/end2end/*.rb']
17end
18
19spec = Gem::Specification.load('grpc.gemspec')
20
21Gem::PackageTask.new(spec) do |pkg|
22end
23
24# Add the extension compiler task
25Rake::ExtensionTask.new('grpc_c', spec) do |ext|
26  ext.source_pattern = '**/*.{c,h}'
27  ext.ext_dir = File.join('src', 'ruby', 'ext', 'grpc')
28  ext.lib_dir = File.join('src', 'ruby', 'lib', 'grpc')
29  ext.cross_compile = true
30  ext.cross_platform = [
31    'x86-mingw32', 'x64-mingw32', 'x64-mingw-ucrt',
32    'x86_64-linux', 'x86-linux',
33    'x86_64-darwin', 'arm64-darwin',
34    'universal-darwin'
35  ]
36  ext.cross_compiling do |spec|
37    spec.files = spec.files.select {
38      |file| file.start_with?(
39        "src/ruby/bin/", "src/ruby/ext/", "src/ruby/lib/", "src/ruby/pb/")
40    }
41    spec.files += %w( etc/roots.pem grpc_c.32-msvcrt.ruby grpc_c.64-msvcrt.ruby grpc_c.64-ucrt.ruby )
42  end
43end
44
45CLEAN.add "src/ruby/lib/grpc/[0-9].[0-9]", "src/ruby/lib/grpc/grpc_c.{bundle,so}"
46
47# Define the test suites
48SPEC_SUITES = [
49  { id: :wrapper, title: 'wrapper layer', files: %w(src/ruby/spec/*.rb) },
50  { id: :idiomatic, title: 'idiomatic layer', dir: %w(src/ruby/spec/generic),
51    tags: ['~bidi', '~server'] },
52  { id: :bidi, title: 'bidi tests', dir: %w(src/ruby/spec/generic),
53    tag: 'bidi' },
54  { id: :server, title: 'rpc server thread tests', dir: %w(src/ruby/spec/generic),
55    tag: 'server' },
56  { id: :pb, title: 'protobuf service tests', dir: %w(src/ruby/spec/pb) }
57]
58namespace :suite do
59  SPEC_SUITES.each do |suite|
60    desc "Run all specs in the #{suite[:title]} spec suite"
61    RSpec::Core::RakeTask.new(suite[:id]) do |t|
62      ENV['COVERAGE_NAME'] = suite[:id].to_s
63      spec_files = []
64      suite[:files].each { |f| spec_files += Dir[f] } if suite[:files]
65
66      if suite[:dir]
67        suite[:dir].each { |f| spec_files += Dir["#{f}/**/*_spec.rb"] }
68      end
69      helper = 'src/ruby/spec/spec_helper.rb'
70      spec_files << helper unless spec_files.include?(helper)
71
72      t.pattern = spec_files
73      t.rspec_opts = "--tag #{suite[:tag]}" if suite[:tag]
74      if suite[:tags]
75        t.rspec_opts = suite[:tags].map { |x| "--tag #{x}" }.join(' ')
76      end
77    end
78  end
79end
80
81desc 'Build the Windows gRPC DLLs for Ruby. The argument contains the list of platforms for which to build dll. Empty placeholder files will be created for platforms that were not selected.'
82task 'dlls', [:plat] do |t, args|
83  grpc_config = ENV['GRPC_CONFIG'] || 'opt'
84  verbose = ENV['V'] || '0'
85  # use env variable to set artifact build paralellism
86  nproc_override = ENV['GRPC_RUBY_BUILD_PROCS'] || `nproc`.strip
87  plat_list = args[:plat]
88
89  build_configs = [
90    { cross: 'x86_64-w64-mingw32', out: 'grpc_c.64-ucrt.ruby', platform: 'x64-mingw-ucrt' },
91    { cross: 'x86_64-w64-mingw32', out: 'grpc_c.64-msvcrt.ruby', platform: 'x64-mingw32' },
92    { cross: 'i686-w64-mingw32', out: 'grpc_c.32-msvcrt.ruby', platform: 'x86-mingw32' }
93  ]
94  selected_build_configs = []
95  build_configs.each do |config|
96    if plat_list.include?(config[:platform])
97      # build the DLL (as grpc_c.*.ruby)
98      selected_build_configs.append(config)
99    else
100      # create an empty grpc_c.*.ruby file as a placeholder
101      FileUtils.touch config[:out]
102    end
103  end
104
105  env = 'CPPFLAGS="-D_WIN32_WINNT=0x600 -DNTDDI_VERSION=0x06000000 -DUNICODE -D_UNICODE -Wno-unused-variable -Wno-unused-result -DCARES_STATICLIB -Wno-error=conversion -Wno-sign-compare -Wno-parentheses -Wno-format -DWIN32_LEAN_AND_MEAN" '
106  env += 'CFLAGS="-Wno-incompatible-pointer-types" '
107  env += 'CXXFLAGS="-std=c++14 -fno-exceptions" '
108  env += 'LDFLAGS=-static '
109  env += 'SYSTEM=MINGW32 '
110  env += 'EMBED_ZLIB=true '
111  env += 'EMBED_OPENSSL=true '
112  env += 'EMBED_CARES=true '
113  env += 'BUILDDIR=/tmp '
114  env += "V=#{verbose} "
115  env += "GRPC_RUBY_BUILD_PROCS=#{nproc_override} "
116
117  out = GrpcBuildConfig::CORE_WINDOWS_DLL
118
119  # propagate env variables with ccache configuration to the rake-compiler-dock docker container
120  # and setup ccache symlinks as needed.
121  # TODO(jtattermusch): deduplicate creation of prepare_ccache_cmd
122  prepare_ccache_cmd = "export GRPC_BUILD_ENABLE_CCACHE=\"#{ENV.fetch('GRPC_BUILD_ENABLE_CCACHE', '')}\" && "
123  prepare_ccache_cmd += "export CCACHE_SECONDARY_STORAGE=\"#{ENV.fetch('CCACHE_SECONDARY_STORAGE', '')}\" && "
124  prepare_ccache_cmd += "export PATH=\"$PATH:/usr/local/bin\" && "
125  prepare_ccache_cmd += "source tools/internal_ci/helper_scripts/prepare_ccache_symlinks_rc "
126
127  selected_build_configs.each do |opt|
128    env_comp = "CC=#{opt[:cross]}-gcc "
129    env_comp += "CXX=#{opt[:cross]}-g++ "
130    env_comp += "LD=#{opt[:cross]}-gcc "
131    env_comp += "LDXX=#{opt[:cross]}-g++ "
132    run_rake_compiler(opt[:platform], <<~EOT)
133      #{prepare_ccache_cmd} && \
134      gem update --system --no-document && \
135      #{env} #{env_comp} make -j#{nproc_override} #{out} && \
136      #{opt[:cross]}-strip -x -S #{out} && \
137      cp #{out} #{opt[:out]}
138    EOT
139  end
140end
141
142desc 'Build the native gem file under rake_compiler_dock. Optionally one can pass argument to build only native gem for a chosen platform.'
143task 'gem:native', [:plat] do |t, args|
144  verbose = ENV['V'] || '0'
145
146  grpc_config = ENV['GRPC_CONFIG'] || 'opt'
147  ruby_cc_versions = ['3.2.0', '3.1.0', '3.0.0', '2.7.0', '2.6.0'].join(':')
148  selected_plat = "#{args[:plat]}"
149
150  # use env variable to set artifact build paralellism
151  nproc_override = ENV['GRPC_RUBY_BUILD_PROCS'] || `nproc`.strip
152
153  # propagate env variables with ccache configuration to the rake-compiler-dock docker container
154  # and setup ccache symlinks as needed.
155  prepare_ccache_cmd = "export GRPC_BUILD_ENABLE_CCACHE=\"#{ENV.fetch('GRPC_BUILD_ENABLE_CCACHE', '')}\" && "
156  prepare_ccache_cmd += "export CCACHE_SECONDARY_STORAGE=\"#{ENV.fetch('CCACHE_SECONDARY_STORAGE', '')}\" && "
157  prepare_ccache_cmd += "export PATH=\"$PATH:/usr/local/bin\" && "
158  prepare_ccache_cmd += "source tools/internal_ci/helper_scripts/prepare_ccache_symlinks_rc "
159
160  supported_windows_platforms = ['x86-mingw32', 'x64-mingw32', 'x64-mingw-ucrt']
161  supported_unix_platforms = ['x86_64-linux', 'x86-linux', 'x86_64-darwin', 'arm64-darwin']
162  supported_platforms = supported_windows_platforms + supported_unix_platforms
163
164  if selected_plat.empty?
165    # build everything
166    windows_platforms = supported_windows_platforms
167    unix_platforms = supported_unix_platforms
168  else
169    # build only selected platform
170    if supported_windows_platforms.include?(selected_plat)
171      windows_platforms = [selected_plat]
172      unix_platforms = []
173    elsif supported_unix_platforms.include?(selected_plat)
174      windows_platforms = []
175      unix_platforms = [selected_plat]
176    else
177      fail "Unsupported platform '#{selected_plat}' passed as an argument."
178    end
179  end
180
181  # Create the windows dlls or create the empty placeholders
182  Rake::Task['dlls'].execute(plat: windows_platforms)
183
184  windows_platforms.each do |plat|
185    run_rake_compiler(plat, <<~EOT)
186      #{prepare_ccache_cmd} && \
187      gem update --system --no-document && \
188      bundle && \
189      bundle exec rake clean && \
190      bundle exec rake native:#{plat} pkg/#{spec.full_name}-#{plat}.gem pkg/#{spec.full_name}.gem \
191        RUBY_CC_VERSION=#{ruby_cc_versions} \
192        V=#{verbose} \
193        GRPC_CONFIG=#{grpc_config} \
194        GRPC_RUBY_BUILD_PROCS=#{nproc_override}
195    EOT
196  end
197
198  # Truncate grpc_c.*.ruby files because they're for Windows only and we don't want
199  # them to take up space in the gems that don't target windows.
200  File.truncate('grpc_c.32-msvcrt.ruby', 0)
201  File.truncate('grpc_c.64-msvcrt.ruby', 0)
202  File.truncate('grpc_c.64-ucrt.ruby', 0)
203
204  unix_platforms.each do |plat|
205    run_rake_compiler(plat, <<~EOT)
206      #{prepare_ccache_cmd} && \
207      gem update --system --no-document && \
208      bundle && \
209      bundle exec rake clean && \
210      bundle exec rake native:#{plat} pkg/#{spec.full_name}-#{plat}.gem pkg/#{spec.full_name}.gem \
211        RUBY_CC_VERSION=#{ruby_cc_versions} \
212        V=#{verbose} \
213        GRPC_CONFIG=#{grpc_config} \
214        GRPC_RUBY_BUILD_PROCS=#{nproc_override}
215    EOT
216  end
217end
218
219# Define dependencies between the suites.
220task 'suite:wrapper' => [:compile, :rubocop]
221task 'suite:idiomatic' => 'suite:wrapper'
222task 'suite:bidi' => 'suite:wrapper'
223task 'suite:server' => 'suite:wrapper'
224task 'suite:pb' => 'suite:server'
225
226desc 'Compiles the gRPC extension then runs all the tests'
227task all: ['suite:idiomatic', 'suite:bidi', 'suite:pb', 'suite:server']
228task default: :all
229