File: mkmf.rb

package info (click to toggle)
ruby-rb-sys 0.9.87-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 192 kB
  • sloc: ruby: 1,186; makefile: 4
file content (365 lines) | stat: -rw-r--r-- 12,679 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# frozen_string_literal: true

require "rubygems/ext"
require "shellwords"
require_relative "cargo_builder"
require_relative "mkmf/config"

# rubocop:disable Style/GlobalVars
# Root module
module RbSys
  # Helper class for creating Rust Makefiles
  module Mkmf
    # @api private
    GLOBAL_RUSTFLAGS = ["--cfg=rb_sys_gem"]

    # Helper for building Rust extensions by creating a Ruby compatible makefile
    # for Rust. By using this class, your rust extension will be 100% compatible
    # with the rake-compiler gem, which allows for easy cross compilation.
    #
    # @example Basic
    #   require 'mkmf'
    #   require 'rb_sys/mkmf'
    #
    #   create_rust_makefile("my_extension") #=> Generate a Makefile in the current directory
    #
    # @example Configure a custom build profile
    #   require 'mkmf'
    #   require 'rb_sys/mkmf'
    #
    #   create_rust_makefile("my_extension") do |r|
    #     # All of these are optional
    #     r.env = { 'FOO' => 'bar' }
    #     r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :dev).to_sym
    #     r.features = %w[some_cargo_feature]
    #     r.rustflags = %w[--cfg=foo]
    #     r.target_dir = "some/target/dir"
    #   end
    def create_rust_makefile(target, &blk)
      if target.include?("/")
        target_prefix, target = File.split(target)
        target_prefix[0, 0] = "/"
      else
        target_prefix = ""
      end

      spec = Struct.new(:name, :metadata).new(target, {})
      cargo_builder = CargoBuilder.new(spec)
      builder = Config.new(cargo_builder)

      yield builder if blk

      srcprefix = File.join("$(srcdir)", builder.ext_dir.gsub(/\A\.\/?/, "")).chomp("/")
      RbConfig.expand(srcdir = srcprefix.dup)

      full_cargo_command = cargo_command(srcdir, builder)
      global_rustflags = GLOBAL_RUSTFLAGS.dup
      global_rustflags << "--cfg=rb_sys_use_stable_api_compiled_fallback" if builder.use_stable_api_compiled_fallback?

      make_install = +<<~MAKE
        #{conditional_assign("RB_SYS_BUILD_DIR", File.join(Dir.pwd, ".rb-sys"))}
        #{conditional_assign("CARGO", "cargo")}
        #{conditional_assign("CARGO_BUILD_TARGET", builder.target)}
        #{conditional_assign("SOEXT", builder.so_ext)}
        #{try_load_bundled_libclang(builder)}

        # Determine the prefix Cargo uses for the lib.
        #{if_neq_stmt("$(SOEXT)", "dll")}
        #{conditional_assign("SOEXT_PREFIX", "lib", indent: 1)}
        #{endif_stmt}

        #{set_cargo_profile(builder)}
        #{conditional_assign("RB_SYS_CARGO_FEATURES", builder.features.join(","))}
        #{conditional_assign("RB_SYS_GLOBAL_RUSTFLAGS", global_rustflags.join(" "))}
        #{conditional_assign("RB_SYS_EXTRA_RUSTFLAGS", builder.extra_rustflags.join(" "))}
        #{conditional_assign("RB_SYS_EXTRA_CARGO_ARGS", builder.extra_cargo_args.join(" "))}
        #{conditional_assign("RB_SYS_CARGO_MANIFEST_DIR", builder.manifest_dir)}

        # Set dirname for the profile, since the profiles do not directly map to target dir (i.e. dev -> debug)
        #{if_eq_stmt("$(RB_SYS_CARGO_PROFILE)", "dev")}
        #{conditional_assign("RB_SYS_CARGO_PROFILE_DIR", "debug", indent: 1)}
        #{else_stmt}
        #{conditional_assign("RB_SYS_CARGO_PROFILE_DIR", "$(RB_SYS_CARGO_PROFILE)", indent: 1)}
        #{endif_stmt}

        # Set the build profile (dev, release, etc.).
        #{assign_stmt("RB_SYS_CARGO_PROFILE_FLAG", "--profile $(RB_SYS_CARGO_PROFILE)", indent: 1)}

        # Account for sub-directories when using `--target` argument with Cargo
        #{conditional_assign("RB_SYS_CARGO_TARGET_DIR", "target")}
        #{if_neq_stmt("$(CARGO_BUILD_TARGET)", "")}
        #{assign_stmt("RB_SYS_FULL_TARGET_DIR", "$(RB_SYS_CARGO_TARGET_DIR)/$(CARGO_BUILD_TARGET)", indent: 1)}
        #{else_stmt}
        #{assign_stmt("RB_SYS_FULL_TARGET_DIR", "$(RB_SYS_CARGO_TARGET_DIR)", indent: 1)}
        #{endif_stmt}

        target_prefix = #{target_prefix}
        TARGET_NAME = #{target[/\A\w+/]}
        TARGET_ENTRY = #{RbConfig::CONFIG["EXPORT_PREFIX"]}Init_$(TARGET_NAME)
        RUBYARCHDIR = $(sitearchdir)$(target_prefix)
        TARGET = #{target}
        DLLIB = $(TARGET).#{RbConfig::CONFIG["DLEXT"]}
        RUSTLIBDIR = $(RB_SYS_FULL_TARGET_DIR)/$(RB_SYS_CARGO_PROFILE_DIR)
        RUSTLIB = $(RUSTLIBDIR)/$(SOEXT_PREFIX)$(TARGET_NAME).$(SOEXT)
        TIMESTAMP_DIR = .

        CLEANOBJS = $(RUSTLIBDIR) $(RB_SYS_BUILD_DIR)
        CLEANLIBS = $(DLLIB) $(RUSTLIB)
        RUBYGEMS_CLEAN_DIRS = $(CLEANOBJS) $(CLEANFILES) #{builder.rubygems_clean_dirs.join(" ")}

        #{base_makefile(srcdir)}

        .PHONY: gemclean

        #{if_neq_stmt("$(RB_SYS_VERBOSE)", "")}
        #{assign_stmt("Q", "$(0=@)", indent: 1)}
        #{endif_stmt}

        #{env_vars(builder)}
        #{export_env("RUSTFLAGS", "$(RB_SYS_GLOBAL_RUSTFLAGS) $(RB_SYS_EXTRA_RUSTFLAGS) $(RUSTFLAGS)")}

        FORCE: ;

        #{optional_rust_toolchain(builder)}

        #{timestamp_file("sitearchdir")}:
        \t$(Q) $(MAKEDIRS) $(@D) $(RUBYARCHDIR)
        \t$(Q) $(TOUCH) $@

        $(RUSTLIB): FORCE
        \t$(ECHO) generating $(@) \\("$(RB_SYS_CARGO_PROFILE)"\\)
        \t#{full_cargo_command}

        $(DLLIB): $(RUSTLIB)
        \t$(Q) $(COPY) "$(RUSTLIB)" $@

        install-so: $(DLLIB) #{timestamp_file("sitearchdir")}
        \t$(ECHO) installing $(DLLIB) to $(RUBYARCHDIR)
        \t$(Q) $(MAKEDIRS) $(RUBYARCHDIR)
        \t$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)

        gemclean:
        \t$(ECHO) Cleaning gem artifacts
        \t-$(Q)$(RM_RF) $(RUBYGEMS_CLEAN_DIRS) 2> /dev/null || true

        install: #{builder.clean_after_install ? "install-so gemclean" : "install-so"}

        all: #{$extout ? "install" : "$(DLLIB)"}
      MAKE

      gsub_cargo_command!(make_install, builder: builder)

      File.write("Makefile", make_install)
    end

    private

    def base_makefile(cargo_dir)
      base_makefile = dummy_makefile(__dir__).join("\n")
      base_makefile.gsub!("all install static install-so install-rb", "all static install-rb")
      base_makefile.gsub!(/^srcdir = .*$/, "srcdir = #{cargo_dir}")
      base_makefile
    end

    def cargo_command(cargo_dir, builder)
      builder.ext_dir = cargo_dir
      dest_path = builder.target_dir || File.join(Dir.pwd, "target")
      args = ARGV.dup
      args.shift if args.first == "--"
      cargo_cmd = builder.cargo_command(dest_path, args)
      cmd = Shellwords.join(cargo_cmd)
      cmd.gsub!("\\=", "=")
      cmd.gsub!(/\Acargo rustc/, "$(CARGO) rustc $(RB_SYS_EXTRA_CARGO_ARGS) --manifest-path $(RB_SYS_CARGO_MANIFEST_DIR)/Cargo.toml")
      cmd.gsub!(/-v=\d/, "")
      cmd
    end

    def env_vars(builder)
      lines = []

      if (cc = env_or_makefile_config("CC", builder)) && find_executable(cc)
        lines << assign_stmt("CC", cc)
      end

      if (cxx = env_or_makefile_config("CXX", builder)) && find_executable(cxx)
        lines << assign_stmt("CXX", cxx)
      end

      if (ar = env_or_makefile_config("AR", builder)) && find_executable(ar)
        lines << assign_stmt("AR", ar)
      end

      lines += builder.build_env.map { |k, v| env_line(k, v) }
      lines.compact.join("\n")
    end

    def env_line(k, v)
      return unless v
      export_env(k, strip_cmd(v.gsub("\n", '\n')))
    end

    def strip_cmd(cmd)
      cmd.gsub("-nologo", "").strip
    end

    def env_or_makefile_config(key, builder)
      builder.env[key] || ENV[key] || RbConfig::MAKEFILE_CONFIG[key]
    end

    def gsub_cargo_command!(cargo_command, builder:)
      cargo_command.gsub!(/--profile \w+/, "$(RB_SYS_CARGO_PROFILE_FLAG)")
      cargo_command.gsub!(%r{--features \S+}, "--features $(RB_SYS_CARGO_FEATURES)")
      cargo_command.gsub!(%r{--target \S+}, "--target $(CARGO_BUILD_TARGET)")
      cargo_command.gsub!(/--target-dir (?:(?!--).)+/, "--target-dir $(RB_SYS_CARGO_TARGET_DIR) ")
      cargo_command
    end

    def rust_toolchain_env(builder)
      <<~MAKE
        #{conditional_assign("RB_SYS_RUSTUP_PROFILE", "minimal")}

        # If the user passed true, we assume stable Rust. Otherwise, use what
        # was specified (i.e. RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN=beta)
        #{if_eq_stmt("$(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN)", "true")}
          RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN = stable
        #{endif_stmt}

        # If a $RUST_TARGET is specified (i.e. for rake-compiler-dock), append
        # that to the profile.
        #{if_eq_stmt("$(RUST_TARGET)", "")}
          RB_SYS_DEFAULT_TOOLCHAIN = $(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN)
        #{else_stmt}
          RB_SYS_DEFAULT_TOOLCHAIN = $(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN)-$(RUST_TARGET)
        #{endif_stmt}

        # Since we are forcing the installation of the Rust toolchain, we need
        # to set these env vars unconditionally for the build.
        #{export_env("CARGO_HOME", "$(RB_SYS_BUILD_DIR)/$(RB_SYS_DEFAULT_TOOLCHAIN)/cargo")}
        #{export_env("RUSTUP_HOME", "$(RB_SYS_BUILD_DIR)/$(RB_SYS_DEFAULT_TOOLCHAIN)/rustup")}
        #{export_env("PATH", "$(CARGO_HOME)/bin:$(RUSTUP_HOME)/bin:$(PATH)")}
        #{export_env("RUSTUP_TOOLCHAIN", "$(RB_SYS_DEFAULT_TOOLCHAIN)")}
        #{export_env("CARGO", "$(CARGO_HOME)/bin/cargo")}
      MAKE
    end

    def optional_rust_toolchain(builder)
      <<~MAKE
        #{conditional_assign("RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN", force_install_rust_toolchain?(builder))}

        # Only run if the we are told to explicitly install the Rust toolchain
        #{if_neq_stmt("$(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN)", "false")}
        #{rust_toolchain_env(builder)}

        $(CARGO):
        \t$(Q) $(MAKEDIRS) $(CARGO_HOME) $(RUSTUP_HOME)
        \t$(Q) curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused -fsSL "https://sh.rustup.rs" | sh -s -- --no-modify-path --profile $(RB_SYS_RUSTUP_PROFILE) --default-toolchain none -y
        \t$(Q) $(CARGO_HOME)/bin/rustup toolchain install $(RB_SYS_DEFAULT_TOOLCHAIN) --profile $(RB_SYS_RUSTUP_PROFILE)
        \t$(Q) $(CARGO_HOME)/bin/rustup default $(RB_SYS_DEFAULT_TOOLCHAIN)
        #{install_extra_rustup_targets(builder)}

        $(RUSTLIB): $(CARGO)
        #{endif_stmt}
      MAKE
    end

    def install_extra_rustup_targets(builder)
      builder.extra_rustup_targets.map do |target|
        "\t$(Q) $(CARGO_HOME)/bin/rustup target add #{target}"
      end.join("\n")
    end

    def force_install_rust_toolchain?(builder)
      return builder.force_install_rust_toolchain if builder.force_install_rust_toolchain
      return false unless builder.rubygems_invoked? && builder.auto_install_rust_toolchain

      find_executable("cargo").nil?
    end

    def if_eq_stmt(a, b)
      if $nmake
        "!IF #{a.inspect} == #{b.inspect}"
      else
        "ifeq (#{a},#{b})"
      end
    end

    def if_neq_stmt(a, b)
      if $nmake
        "!IF #{a.inspect} != #{b.inspect}"
      else
        "ifneq (#{a},#{b})"
      end
    end

    def else_stmt
      if $nmake
        "!ELSE"
      else
        "else"
      end
    end

    def endif_stmt
      if $nmake
        "!ENDIF"
      else
        "endif"
      end
    end

    def conditional_assign(a, b, export: false, indent: 0)
      if $nmake
        result = +"!IFNDEF #{a}\n#{a} = #{b}\n!ENDIF\n"
        result << export_env(a, b) if export
        result
      else
        "#{"\t" * indent}#{export ? "export " : ""}#{a} ?= #{b}"
      end
    end

    def assign_stmt(a, b, indent: 0)
      if $nmake
        "#{a} = #{b}"
      else
        "#{"\t" * indent}#{a} = #{b}"
      end
    end

    def export_env(k, v)
      if $nmake
        "!if [set #{k}=#{v}]\n!endif"
      else
        "export #{k} := #{v}"
      end
    end

    def try_load_bundled_libclang(_builder)
      require "libclang"
      assert_libclang_version_valid!
      export_env("LIBCLANG_PATH", Libclang.libdir)
    rescue LoadError
      # If we can't load the bundled libclang, just continue
    end

    def assert_libclang_version_valid!
      libclang_version = Gem::Version.new(Libclang.version)

      if libclang_version < Gem::Version.new("5.0.0")
        raise "libclang version 5.0.0 or greater is required (current #{libclang_version})"
      end

      if libclang_version >= Gem::Version.new("17.0.0")
        raise "libclang version < 17.0.0 or greater is required (current #{libclang_version})"
      end
    end

    def set_cargo_profile(builder)
      return assign_stmt("RB_SYS_CARGO_PROFILE", "release") if builder.rubygems_invoked?

      conditional_assign("RB_SYS_CARGO_PROFILE", builder.profile)
    end
  end
end
# rubocop:enable Style/GlobalVars

include RbSys::Mkmf # rubocop:disable Style/MixinUsage