Top Level Namespace

Extended by:
Ruby2D::DSL
Includes:
Ruby2D

Defined Under Namespace

Modules: Ruby2D Classes: Module, String

Constant Summary collapse

BUILD_DIR =

Our build output lives in build/ under the current directory, and a build wipes it first. A hidden marker lets a later build (or --clean) tell its own output from an unrelated, pre-existing build/ — CMake, Meson, and many other tools default to that name — before deleting anything.

'build'
BUILD_MARKER =
File.join(BUILD_DIR, '.ruby2d')
SDL3_STATIC_LIBS =

Whether all four SDL3 static archives are present in a lib dir

%w[libSDL3.a libSDL3_image.a libSDL3_mixer.a libSDL3_ttf.a].freeze

Constants included from Ruby2D

Ruby2D::Colour, Ruby2D::TextureAtlas, Ruby2D::VERSION

Instance Method Summary collapse

Methods included from Ruby2D::DSL

add_gamepad_mapping, clear, close, elapsed, gamepads, get, off, on, render, request_render, screenshot, set, show, update, window, window=, window?

Methods included from Ruby2D

assets, gem_dir, info, test_audio, test_images, test_media, test_spritesheets, warn

Instance Method Details

#abort_error(msg, spaced: false, indent: 0) ⇒ Object

Print an Error: line and exit non-zero. Writes to stderr rather than stdout, and never returns — so it also reads correctly as the right-hand side of system(cmd) || abort_error(...).



38
39
40
# File 'lib/ruby2d/cli/messages.rb', line 38

def abort_error(msg, spaced: false, indent: 0)
  abort ruby2d_labeled('Error:'.error, msg, spaced, indent)
end

#add_flags(type, flags) ⇒ Object

Add compiler and linker flags



9
10
11
12
13
14
# File 'ext/ruby2d/extconf.rb', line 9

def add_flags(type, flags)
  case type
  when :c  then $CFLAGS  << " #{flags} "
  when :ld then $LDFLAGS << " #{flags} "
  end
end

#add_ld_flags(ld_flags, name, type, dir = nil) ⇒ Object

Add linker flags



178
179
180
181
182
183
184
185
# File 'lib/ruby2d/cli/build.rb', line 178

def add_ld_flags(ld_flags, name, type, dir = nil)
  case type
  when :archive
    ld_flags << "#{shell_escape("#{dir}/lib#{name}.a")} "
  when :framework
    ld_flags << "-Wl,-framework,#{name} "
  end
end

#asset_directives(file) ⇒ Object

Collect the asset directories declared inline with # ruby2d:assets <dir> directives in the app source — the flag-free equivalent of --assets, so an app can carry its own bundling instructions instead of relying on the caller to remember the flag. One directory per directive; repeat the line for several. Paths are relative to the build's working directory, matching --assets. Returns the directories in source order.



81
82
83
84
85
86
# File 'lib/ruby2d/cli/build.rb', line 81

def asset_directives(file)
  File.foreach(file).filter_map do |line|
    m = line.match(/\A\s*#\s*ruby2d:assets\s+(\S.*?)\s*\z/)
    m && m[1]
  end
end

#build(targets, ruby2d_app) ⇒ Object

Build the user's application for one or more targets



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
# File 'lib/ruby2d/cli/build.rb', line 313

def build(targets, ruby2d_app)
  targets = [targets] unless targets.is_a?(Array)

  compile(ruby2d_app)

  # Asset directories to bundle: the `--assets` flag plus any `# ruby2d:assets
  # <dir>` directives in the app source. Each is bundled at its own relative
  # path — mounted into the WebAssembly virtual filesystem for the web build,
  # copied next to the executable for the native build. Validate once here so a
  # missing directory fails before either target starts compiling.
  @asset_dirs = ([@assets_dir] + asset_directives(ruby2d_app)).compact.uniq
  @asset_dirs.each { |dir| check_asset_dir(dir) }

  targets.each do |target|
    case target
    when :native
      compile_native
    when :web
      compile_web
    end
  end

  # Remove intermediate build files. `--debug` keeps them all — including the
  # assembled `build/app.c`, for anyone who wants to compile it themselves.
  unless @debug
    FileUtils.rm(Dir.glob('build/*.rb'))
    FileUtils.rm(Dir.glob('build/*.c'))
  end

  # Trailing blank line so the output isn't flush against the next shell prompt.
  puts
end

#bundled_platform_dirObject

The bundled platform dir inside the gem — populated for RELEASE_PLATFORMS.



122
123
124
# File 'lib/ruby2d/cli/build.rb', line 122

def bundled_platform_dir
  AssetsTarget.platform_dir(root: Ruby2D.assets)
end

#cache_platform_dirObject

The platform dir a ruby2d setup build writes into, outside the gem.



127
128
129
# File 'lib/ruby2d/cli/build.rb', line 127

def cache_platform_dir
  AssetsTarget.platform_dir(root: AssetsTarget.cache_root)
end

#cache_stamp_ok?(platform_dir) ⇒ Boolean

A ruby2d setup cache build is stamped with the ruby2d version that produced it; only reuse it for a matching version (a gem upgrade may pin newer SDL, so stale cache libs must not be linked against this version's headers/sources).

Returns:

  • (Boolean)


134
135
136
137
# File 'lib/ruby2d/cli/build.rb', line 134

def cache_stamp_ok?(dir)
  stamp = File.join(dir, '.ruby2d-version')
  File.exist?(stamp) && File.read(stamp).strip == Ruby2D::VERSION
end

#check_asset_dir(dir) ⇒ Object

Validate an asset directory (from --assets or a # ruby2d:assets directive) before it's bundled. A missing dir aborts the build rather than ship an app that can't find its assets. Both builds bundle the dir at the path given — the web VFS mounts it there, the native build copies it there next to the executable — so an absolute dir lands at that same absolute path, which the relative references apps use won't find; warn rather than silently mislead.



451
452
453
454
455
456
457
458
459
460
# File 'lib/ruby2d/cli/build.rb', line 451

def check_asset_dir(dir)
  unless Dir.exist?(dir)
    error "asset directory not found: #{dir}"
    exit 1
  end
  return unless File.absolute_path?(dir)

  warning "assets dir `#{dir}` is absolute, so it's bundled at that same " \
          'path. Reference it by that absolute path, or use a relative dir (e.g. `media`) instead.'
end

#clean_up(cmd = nil) ⇒ Object

Clean up the build directory



615
616
617
618
619
620
621
622
# File 'lib/ruby2d/cli/build.rb', line 615

def clean_up(cmd = nil)
  if cmd == :all
    refuse_if_foreign_build_dir
    step 'Cleaning build directory'
    FileUtils.rm_rf Dir.glob("#{BUILD_DIR}/*")
    puts
  end
end

#cmd_echo(cmd) ⇒ Object

Echo a command under --debug, abbreviating the gem dir to keep it readable. Substitutes both the escaped and plain gem dir so it works whether or not the path needed shell-escaping.



34
35
36
37
# File 'lib/ruby2d/cli/build.rb', line 34

def cmd_echo(cmd)
  display_cmd = cmd.gsub(shell_escape(Ruby2D.gem_dir), '$RUBY2D').gsub(Ruby2D.gem_dir, '$RUBY2D')
  puts "  #{"$ #{display_cmd}".dim}\n\n"
end

#compile(ruby2d_app) ⇒ Object

Compile Ruby source to mruby bytecode and assemble into a single C file



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
# File 'lib/ruby2d/cli/build.rb', line 213

def compile(ruby2d_app)

  # Check if source file provided is good
  if !ruby2d_app
    error 'Please provide a Ruby file to build.'
    exit 1
  elsif !File.exist? ruby2d_app
    error "Can't find file: #{ruby2d_app}"
    exit 1
  end

  # Add debugging information to produce backtrace
  debug_flag = @debug ? '-g' : ''

  # Clear and create build directory, refusing to wipe a foreign `build/` and
  # marking ours so later builds recognize it.
  refuse_if_foreign_build_dir
  FileUtils.rm_rf Dir.glob("#{BUILD_DIR}/*")
  FileUtils.mkdir_p BUILD_DIR
  FileUtils.touch BUILD_MARKER

  # Compiling Ruby to bytecode is a sub-second internal step with no artifact a
  # user runs — show its banner (and the `mrbc` echoes below) only with `--debug`.
  step "Compiling #{ruby2d_app}" if @debug

  # Assemble Ruby 2D library files into one '.rb' file

  ruby2d_lib_dir = "#{Ruby2D.gem_dir}/lib/ruby2d/"

  ruby2d_lib = ''
  @ruby2d_lib_files.each do |f|
    ruby2d_lib << File.read("#{ruby2d_lib_dir + f}.rb") + "\n\n"
  end

  # Make Ruby2D classes and DSL methods available at the top level
  ruby2d_lib << "include Ruby2D\nextend Ruby2D::DSL\n"

  File.write('build/ruby2d_lib.rb', ruby2d_lib)

  # Assemble the Ruby 2D C extension files into one '.c' file

  ruby2d_ext_dir = "#{Ruby2D.gem_dir}/ext/ruby2d/"

  ruby2d_ext = "#define MRUBY 1\n\n"
  c_files = Dir["#{ruby2d_ext_dir}*.c"].sort
  main_file = c_files.delete("#{ruby2d_ext_dir}ruby2d.c")
  c_files.unshift(main_file) if main_file
  c_files.each { |c_file| ruby2d_ext << File.read(c_file) }

  File.write('build/ruby2d_ext.c', ruby2d_ext)

  # Find the `mrbc` executable
  mrbc = find_mrbc
  unless mrbc
    error "Can't find `mrbc`, the mruby compiler."
    puts 'Run `ruby2d setup` to build it, or install mruby so `mrbc` is on your PATH.'
    exit 1
  end

  # Compile the Ruby 2D lib (`.rb` files) to mruby bytecode. `run_cmd` doesn't
  # check the result, so do it here — a failure means the assembled bytecode
  # files won't exist, and the combine step below would crash on a missing file.
  run_cmd "#{shell_escape(mrbc)} #{debug_flag} -Bruby2d_lib -obuild/ruby2d_lib.c build/ruby2d_lib.rb"
  unless $?.success?
    error 'Failed to compile the Ruby 2D library.'
    exit 1
  end

  # Stage the user's source (requires blanked — see strip_require) and compile it.
  # `mrbc` reports diagnostics against the path it's given, so capture its output
  # and rewrite the staging path back to the original filename: a syntax error
  # then points at `asteroids.rb:LINE`, the file the user wrote, not the internal
  # copy. Line numbers already line up because strip_require preserves them.
  staged = 'build/ruby2d_app.rb'
  File.write(staged, strip_require(ruby2d_app))
  app_cmd = "#{shell_escape(mrbc)} #{debug_flag} -Bruby2d_app -obuild/ruby2d_app.c #{staged}"
  cmd_echo(app_cmd) if @debug
  app_output = `#{app_cmd} 2>&1`
  app_result = $?
  print app_output.gsub(staged) { ruby2d_app } unless app_output.empty?
  unless app_result.success?
    error "Failed to compile #{ruby2d_app}."
    puts 'Check the error above for syntax issues or Ruby features mruby does not support.'
    exit 1
  end

  # Combine contents of C source files and bytecode into one file
  File.open('build/app.c', 'w') do |f|
    ['ruby2d_app', 'ruby2d_lib', 'ruby2d_ext'].each do |c_file|
      f << File.read("build/#{c_file}.c") << "\n\n"
    end
  end

  # `build/app.c` is an intermediate the native/web steps consume, not something
  # the user runs — surface it only with `--debug`, where it's also kept.
  wrote 'build/app.c' if @debug
end

#compile_nativeObject

Create a native executable using the available C compiler



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/ruby2d/cli/build.rb', line 348

def compile_native

  # Get include directories
  incl_dir_ruby2d = "#{Ruby2D.gem_dir}/ext/ruby2d/"
  incl_dir_deps = deps_include_dir

  # Set C flags, if any
  c_flags = ''

  # Add library search directory
  ld_flags = ''
  # Dependent SDL archives before the base `libSDL3.a` so single-pass GNU ld
  # (MinGW on Windows, and Linux/BSD ld) resolves their symbols; mruby is
  # independent of SDL. Mirrors the order in ext/ruby2d/extconf.rb.
  libs = %w[mruby SDL3_image SDL3_mixer SDL3_ttf SDL3]
  if (platform_dir = deps_platform_dir)
    # Bundled or `ruby2d setup`-built static archives. macOS's ld64 is order-independent.
    ld_dir = File.join(platform_dir, 'lib')
    libs.each { |name| add_ld_flags(ld_flags, name, :archive, ld_dir) }
  else
    # No static libs for this target (Linux/BSD aren't in RELEASE_PLATFORMS and
    # `ruby2d setup` hasn't built them): link system-installed SDL3 + mruby, the
    # same fallback find_mrbc uses for `mrbc`.
    libs.each { |name| ld_flags << "-l#{name} " }
  end

  # Add compiler flags for each platform
  case AssetsTarget.host_os

  when 'macos'
    %w[AVFoundation AudioToolbox Carbon Cocoa CoreAudio CoreHaptics
       CoreMedia ForceFeedback GameController IOKit Metal QuartzCore
       UniformTypeIdentifiers].each do |name|
      add_ld_flags(ld_flags, name, :framework)
    end

  when 'windows'
    ld_flags << '-lgdi32 -lhid -limm32 -lole32 -loleaut32 -lrpcrt4 -lsetupapi -lusp10 -luuid -lversion -lwinmm -lws2_32'

  when 'linux', 'bsd'
    ld_flags << '-lm'
  end

  # Check for a C compiler up front so a missing toolchain gives a clear message
  # instead of a bare `failed` on exit 127 (like the `mrbc` check). A missing C
  # compiler is a hard error — native can't build without it — whereas a missing
  # `emcc` only skips the optional web build. Honor $CC if set, else default `cc`.
  cc = ENV['CC'] || 'cc'
  if find_executable(cc).nil?
    error "Can't find `#{cc}`, a C compiler. Install a C toolchain (e.g. Xcode Command Line Tools or build-essential) or set $CC."
    exit 1
  end

  # Compile the app
  step 'Building native'
  puts "    #{"for #{native_target}".dim}"
  FileUtils.mkdir_p 'build/native'
  run_cmd "#{shell_escape(cc)} #{c_flags} -I#{shell_escape(incl_dir_ruby2d)} -I#{shell_escape(incl_dir_deps)} build/app.c #{ld_flags} -o build/native/app"

  unless $?.success?
    error 'Native build failed.'
    unless deps_platform_dir
      puts "No bundled libraries for #{AssetsTarget.target_id}. Run `ruby2d setup` to build"
      puts 'them, or install SDL3 + mruby with your system package manager.'
    end
    exit 1
  end

  # Bundle the default font next to the executable so apps using the built-in
  # font (`Text.new('…')` with no `font:`) resolve it at runtime. The native app
  # reads `ruby2d/fonts/…` relative to its working directory — which
  # `ruby2d launch --native` sets to `build/native` — mirroring the WASM preload.
  fonts_src = "#{Ruby2D.assets}/resources/fonts"
  if Dir.exist?(fonts_src)
    fonts_dest = 'build/native/ruby2d/fonts'
    FileUtils.mkdir_p fonts_dest
    FileUtils.cp_r "#{fonts_src}/.", fonts_dest
  end

  # Bundle each declared asset directory next to the executable — the native
  # counterpart to the web build's virtual-filesystem preload. The app resolves
  # them at runtime from its working directory (`build/native`, set by
  # `ruby2d launch --native`), so a dir mounts at the same relative path it was
  # given, matching how the app references it (`Image.new('media/x.png')`).
  @asset_dirs.each do |dir|
    dest = File.join('build/native', dir)
    FileUtils.mkdir_p File.dirname(dest)
    FileUtils.cp_r dir, dest
  end

  create_macos_bundle if AssetsTarget.host_os == 'macos'
  wrote 'build/native/app'
  wrote 'build/native/App.app' if AssetsTarget.host_os == 'macos'
  puts "    #{'Run `ruby2d launch --native` to view'.dim}"
end

#compile_webObject

Create a WebAssembly executable using Emscripten



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'lib/ruby2d/cli/build.rb', line 464

def compile_web
  # When Emscripten is missing: skip the web build for a default `ruby2d build`
  # (so the native app still gets produced), but fail loudly when `--web` was
  # passed explicitly — the user asked for web specifically, so it shouldn't pass
  # silently (e.g. in `ruby2d build --web && deploy`).
  if find_executable('emcc').nil?
    if @web_explicit
      error "Can't find `emcc`. Install the Emscripten SDK and source emsdk_env.sh", spaced: true
      exit 1
    end
    puts "\n  #{'Skipping web build — Emscripten (emcc) not found'.dim}"
    return
  end

  step 'Building web'
  FileUtils.mkdir_p 'build/web'

  incl_dir_ruby2d = "#{Ruby2D.gem_dir}/ext/ruby2d/"
  incl_dir_deps = "#{Ruby2D.assets}/platform/include/"

  wasm_lib_dir = "#{Ruby2D.assets}/platform/wasm/lib"
  ld_flags = Dir["#{wasm_lib_dir}/*.a"].map { |f| shell_escape(f) }.join(' ')

  # Always bundle the default font for WASM builds
  fonts_dir = "#{Ruby2D.assets}/resources/fonts"
  preload_flag = "--preload-file #{shell_escape("#{fonts_dir}@ruby2d/fonts")}"

  # Bundle each requested asset directory — from `--assets` and any
  # `# ruby2d:assets` directive — into the virtual filesystem at its own path
  # (the `src@dst` map uses the same string for both). Validated in `build`.
  @asset_dirs.each { |dir| preload_flag += " --preload-file #{shell_escape("#{dir}@#{dir}")}" }

  # Faster page loads: restrict the JS glue to the browser environment (drops the
  # Node/worker probing) and, for release builds, minify it with Closure. Closure
  # is slow, so skip it for --debug (keep iteration fast and the glue readable);
  # the environment trim is cheap and always applied.
  web_opt_flags = '-sENVIRONMENT=web'
  web_opt_flags += ' --closure 1' unless @debug

  # The vendored wasm libmruby.a is built with MRB_NO_BOXING (floats inline in
  # mrb_value — the default 32-bit word boxing heap-allocates every Float).
  # The boxing mode is ABI: app.c includes the mruby headers, so it must define
  # the same mode or values are read with mismatched layouts at runtime.
  mruby_abi_flag = '-DMRB_NO_BOXING'

  # Start the wasm heap at 64MB instead of Emscripten's 16MB default. Growing
  # is kept as a safety valve, but each mid-game `memory.grow` detaches and
  # recopies the heap — a visible frame hitch — and reaching 64MB from 16MB
  # takes ~8 geometric growth steps scattered through early gameplay. Memory
  # isn't stored in the binary, so this doesn't change the download size;
  # browsers commit the pages lazily.
  memory_flags = '-sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH'

  if @single_file
    # A custom template becomes Emscripten's shell file (it must contain the
    # `{{{ SCRIPT }}}` placeholder); otherwise emcc emits its default shell.
    shell_flag = @template ? "--shell-file #{shell_escape(@template)} " : ''
    run_cmd "emcc -O3 #{mruby_abi_flag} -I#{shell_escape(incl_dir_ruby2d)} -I#{shell_escape(incl_dir_deps)} "\
            "-sUSE_SDL=0 -sSINGLE_FILE #{memory_flags} #{web_opt_flags} #{shell_flag}"\
            "build/app.c #{ld_flags} #{preload_flag} "\
            "-o build/web/app.html"

    unless $?.success?
      error 'Web build failed.'
      exit 1
    end

    wrote 'build/web/app.html'
  else
    run_cmd "emcc -O3 #{mruby_abi_flag} -I#{shell_escape(incl_dir_ruby2d)} -I#{shell_escape(incl_dir_deps)} "\
            "-sUSE_SDL=0 #{memory_flags} #{web_opt_flags} "\
            "build/app.c #{ld_flags} #{preload_flag} "\
            "-o build/web/app.js"

    unless $?.success?
      error 'Web build failed.'
      exit 1
    end

    # Use the caller's template if given, else the bundled default. Either way
    # it must load `app.js` (see the bundled `template.html` for the contract).
    FileUtils.cp(@template || "#{Ruby2D.assets}/resources/web/template.html", 'build/web/app.html')
    wrote 'build/web/app.html'
    wrote 'build/web/app.js'
    wrote 'build/web/app.wasm'
    wrote 'build/web/app.data'
  end
  puts "    #{'Run `ruby2d launch --web` to view'.dim}"
end

#create_macos_bundleObject

Build an app bundle for macOS



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
# File 'lib/ruby2d/cli/build.rb', line 556

def create_macos_bundle

  # Property list source for the bundle
  info_plist = %(
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleExecutable</key>
  <string>app</string>
  <key>CFBundleIconFile</key>
  <string>app.icns</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>NSHighResolutionCapable</key>
  <string>True</string>
</dict>
</plist>
)

  # Create directories
  FileUtils.mkpath 'build/native/App.app/Contents/MacOS'
  FileUtils.mkpath 'build/native/App.app/Contents/Resources'

  # Create Info.plist and copy over assets
  File.open('build/native/App.app/Contents/Info.plist', 'w') { |f| f.write(info_plist) }
  FileUtils.cp 'build/native/app', 'build/native/App.app/Contents/MacOS/'

  # Bundle the runtime resources (default font) next to the executable. The app
  # chdirs to its own directory at startup, so it resolves `ruby2d/fonts/…`
  # here when launched from Finder (where the working directory is otherwise `/`).
  if Dir.exist?('build/native/ruby2d')
    FileUtils.cp_r 'build/native/ruby2d', 'build/native/App.app/Contents/MacOS/'
  end

  # Bundle any declared asset directories (copied next to build/native/app
  # earlier) alongside the executable inside the bundle too, preserving their
  # relative paths, so a Finder-launched app resolves them the same way.
  @asset_dirs.each do |dir|
    src = File.join('build/native', dir)
    next unless Dir.exist?(src)

    dest = File.join('build/native/App.app/Contents/MacOS', dir)
    FileUtils.mkdir_p File.dirname(dest)
    FileUtils.cp_r src, dest
  end

  # Bundle the icon referenced by CFBundleIconFile (the Ruby 2D default), so the
  # plist's `app.icns` reference resolves instead of dangling.
  icon = "#{Ruby2D.assets}/resources/icons/icon.icns"
  FileUtils.cp icon, 'build/native/App.app/Contents/Resources/app.icns' if File.exist?(icon)
end

#deps_include_dirObject

The SDL3 + mruby headers matching the resolved libraries (platform/include is a sibling of the per-target lib dir). Falls back to the bundled headers for the system-lib build, where they still describe the pinned API.



155
156
157
158
# File 'lib/ruby2d/cli/build.rb', line 155

def deps_include_dir
  dir = deps_platform_dir
  dir ? File.join(File.dirname(dir), 'include') : "#{Ruby2D.assets}/platform/include"
end

#deps_platform_dirObject

The platform dir holding this target's static libs — bundled first, then a stamped cache build. nil if neither has them, in which case the native build links system-installed SDL3 + mruby (Linux/BSD, or before ruby2d setup).



142
143
144
145
146
147
148
149
150
# File 'lib/ruby2d/cli/build.rb', line 142

def deps_platform_dir
  bundled = bundled_platform_dir
  return bundled if File.exist?(File.join(bundled, 'lib', 'libSDL3.a'))

  cache = cache_platform_dir
  return cache if File.exist?(File.join(cache, 'lib', 'libSDL3.a')) && cache_stamp_ok?(cache)

  nil
end

#error(msg, spaced: false, indent: 0) ⇒ Object



23
24
25
# File 'lib/ruby2d/cli/messages.rb', line 23

def error(msg, spaced: false, indent: 0)
  puts ruby2d_labeled('Error:'.error, msg, spaced, indent)
end

#find_executable(name) ⇒ Object

Locate an executable on PATH. Portable replacement for which, which is Unix-only (Windows cmd.exe has no which); tries Windows executable extensions when running there.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ruby2d/cli/build.rb', line 92

def find_executable(name)
  exts = AssetsTarget.host_os == 'windows' ? ['.exe', '.bat', '.cmd', ''] : ['']

  # An explicit path (e.g. `CC=/usr/bin/clang`) is used as-is, not searched on
  # PATH — `File.join(dir, '/usr/bin/clang')` would collapse to a bogus path.
  if name.include?(File::SEPARATOR) || (File::ALT_SEPARATOR && name.include?(File::ALT_SEPARATOR))
    exts.each do |ext|
      candidate = "#{name}#{ext}"
      return candidate if File.file?(candidate) && File.executable?(candidate)
    end
    return nil
  end

  ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |dir|
    next if dir.empty?
    exts.each do |ext|
      candidate = File.join(dir, "#{name}#{ext}")
      return candidate if File.file?(candidate) && File.executable?(candidate)
    end
  end
  nil
end

#find_mrbcObject

Find the mrbc executable: prefer bundled assets, then a ruby2d setup cache build, then $PATH.



162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/ruby2d/cli/build.rb', line 162

def find_mrbc
  name = AssetsTarget.host_os == 'windows' ? 'mrbc.exe' : 'mrbc'

  bundled = File.join(bundled_platform_dir, 'bin', name)
  return bundled if File.exist?(bundled)

  cache = cache_platform_dir
  cached_mrbc = File.join(cache, 'bin', name)
  return cached_mrbc if File.exist?(cached_mrbc) && cache_stamp_ok?(cache)

  # Fall back to system-installed mrbc
  find_executable('mrbc')
end

#launch_nativeObject

Launch a native app, passing its stdio through so console output is visible.



16
17
18
19
20
21
22
23
24
25
# File 'lib/ruby2d/cli/launch.rb', line 16

def launch_native
  exe = AssetsTarget.host_os == 'windows' ? 'app.exe' : 'app'
  unless File.exist?("build/native/#{exe}")
    error 'No native app found. Run `ruby2d build --native` first.'
    exit 1
  end
  # Run with the build dir as the working directory so the app resolves its
  # bundled media by relative path, the way the built app expects.
  system(File.expand_path("build/native/#{exe}"), chdir: 'build/native')
end

#launch_webObject

Launch a web app



29
30
31
32
33
34
35
# File 'lib/ruby2d/cli/launch.rb', line 29

def launch_web
  unless File.exist?('build/web/app.html')
    error 'No web app found. Run `ruby2d build --web` first.'
    exit 1
  end
  serve(dir: File.expand_path('build/web'), port: 8080, path: 'app.html')
end

#native_targetObject

A friendly label for the host platform the native build targets (e.g. "macOS (arm64)"). ruby2d build doesn't cross-compile — the executable is for this machine only — so naming it avoids the "is this a universal binary?" doubt.



57
58
59
60
61
# File 'lib/ruby2d/cli/build.rb', line 57

def native_target
  os = { 'macos' => 'macOS', 'windows' => 'Windows',
         'linux' => 'Linux', 'bsd' => 'BSD' }.fetch(AssetsTarget.host_os, AssetsTarget.host_os)
  "#{os} (#{AssetsTarget.host_arch})"
end

#note(msg, spaced: false, indent: 0) ⇒ Object



31
32
33
# File 'lib/ruby2d/cli/messages.rb', line 31

def note(msg, spaced: false, indent: 0)
  puts ruby2d_labeled('Note:'.bold, msg, spaced, indent)
end

#refuse_if_foreign_build_dirObject

Refuse to wipe build/ when it exists, holds files, and isn't ours — so running ruby2d build in a project that already uses build/ for something else can't silently delete it. Dir.glob skips the dotfile marker, so the emptiness check and the delete target stay consistent.



199
200
201
202
203
204
205
206
207
# File 'lib/ruby2d/cli/build.rb', line 199

def refuse_if_foreign_build_dir
  return unless Dir.exist?(BUILD_DIR)
  return if File.exist?(BUILD_MARKER)
  return if Dir.glob("#{BUILD_DIR}/*").empty?

  error "A `#{BUILD_DIR}/` directory already exists here and wasn't created by Ruby 2D."
  puts 'Refusing to delete its contents. Remove it yourself, or run `ruby2d build` from a different directory.'
  exit 1
end

#ruby2d_labeled(label, msg, spaced, indent) ⇒ Object

Compose a labeled line. spaced adds a blank line above, for a message that interrupts a run of build output; indent matches the nesting of the output around it (task bodies indent their results by four).

The ruby2d_ prefix keeps this clear of mkmf's own message, which is in scope while extconf.rb runs.



19
20
21
# File 'lib/ruby2d/cli/messages.rb', line 19

def ruby2d_labeled(label, msg, spaced, indent)
  "#{"\n" if spaced}#{' ' * indent}#{label} #{msg}"
end

#run_cmd(cmd) ⇒ Object



26
27
28
29
# File 'lib/ruby2d/cli/build.rb', line 26

def run_cmd(cmd)
  cmd_echo(cmd) if @debug
  system cmd
end

#serve(dir:, port: 8080, path: '') ⇒ Object

Serve a directory over HTTP and open it in the browser. Backed by a minimal socket-based server (no WEBrick dependency) — see static_server.rb.



10
11
12
# File 'lib/ruby2d/cli/launch.rb', line 10

def serve(dir:, port: 8080, path: '')
  Ruby2D::CLI::StaticServer.serve(dir: dir, port: port, path: path)
end

#setup(force: false, clean: false, yes: false) ⇒ Object

Build the native SDL3 + mruby static libraries for the current platform.



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
# File 'lib/ruby2d/cli/setup.rb', line 160

def setup(force: false, clean: false, yes: false)
  if clean
    # `--clean` is standalone: it only removes. Say so rather than letting a
    # combined `--force` look like it did something (matches `build --clean`).
    note '`--clean` only removes the built libraries; `--force` was ignored.' if force
    return setup_clean(yes: yes)
  end

  target = AssetsTarget.target_id
  cache  = AssetsTarget.cache_root
  cache_platform = AssetsTarget.platform_dir(root: cache)
  stamp = File.join(cache_platform, '.ruby2d-version')

  # Already provided? The gem bundles prebuilt libraries for RELEASE_PLATFORMS.
  bundled = AssetsTarget.platform_dir(root: Ruby2D.assets)
  bundled_exists = File.exist?(File.join(bundled, 'lib', 'libSDL3.a'))
  if !force && bundled_exists
    setup_step 'Native dependencies ready'
    puts "    #{"#{setup_platform_label} — bundled with the gem".dim}"
    puts "\n  Nothing to build. Run #{'ruby2d build --native <app>.rb'.bold} to build an app.\n\n"
    return
  end

  # Already built for this ruby2d version? Skip unless --force.
  built = File.exist?(File.join(cache_platform, 'lib', 'libSDL3.a')) &&
          File.exist?(stamp) && File.read(stamp).strip == Ruby2D::VERSION
  if built && !force
    setup_step 'Native dependencies ready'
    puts "    #{"#{setup_platform_label} — built at #{setup_tildify(cache_platform)}".dim}"
    puts "\n  Already built. Re-run with #{'--force'.bold} to rebuild, or #{'ruby2d build --native <app>.rb'.bold} to build an app.\n\n"
    return
  end

  setup_preflight!

  # Header — surface the platform and cache location up front (again at the
  # end), spell out what the build entails, and confirm before starting: this
  # downloads sources, compiles for minutes, and rebuilds the installed gem.
  setup_step 'Set up native dependencies'
  puts
  puts "    #{'Platform'.dim}   #{setup_platform_label}  #{"(#{target})".dim}"
  puts "    #{'Location'.dim}   #{setup_tildify(cache_platform)}"
  puts "\n  #{'This will:'.bold}"
  if AssetsTarget.host_os == 'windows'
    puts '    • Offer to install any missing build tools (git, CMake) via MSYS2'
  end
  puts '    • Download the SDL3 and mruby sources with git'
  puts '    • Compile them into static libraries — this takes several minutes'
  puts "    • Rebuild the Ruby 2D extension with #{'gem pristine ruby2d'.bold}"
  puts "\n  Sources and build files go under the location above; only the last"
  puts '  step touches anything outside it.'

  # SDL links a lot of the system's development libraries — windowing, audio,
  # and more — and the full set is distro-specific and long, so point at SDL's
  # own list rather than trying to enumerate it. Preflight has already confirmed
  # the one hard requirement (X11 or Wayland); this covers the rest, whose
  # absence quietly disables features (e.g. no audio) instead of failing loudly.
  if AssetsTarget.host_os == 'linux'
    note "SDL builds against your system's development libraries — windowing,", spaced: true
    puts '  audio, and more. Install the packages for your distribution before continuing:'
    puts '  https://wiki.libsdl.org/SDL3/README-linux'
  end

  # Only `--force` gets here on a platform the gem already bundles, and a build
  # resolves bundled libraries first (see cli/build's deps_platform_dir) — so
  # this build won't be linked against. Worth saying before spending minutes on
  # it, rather than refusing: rebuilding is still a fair way to check the
  # pinned sources compile on this machine.
  if bundled_exists
    warning "This platform's libraries are bundled with the gem, and a", spaced: true
    puts "  build prefers those — so #{'ruby2d build'.bold} won't link what this produces."
  end

  setup_confirm!(yes: yes)

  setup_step 'Building native dependencies'
  puts "    #{'$ rake sdl mruby'.dim}\n\n"

  # Redirect sources, intermediates, and output to the cache so nothing is
  # written into the gem dir. Scope the override to the build subprocess (env
  # hash) so it doesn't leak into the `gem pristine` below — that step needs the
  # standard bundled -> cache resolution. The assets Rakefile carries the pinned
  # versions, the cmake/mruby build steps, and (on Windows) the pacman bootstrap.
  ok = Dir.chdir(Ruby2D.assets) do
    system({ 'RUBY2D_ASSETS_ROOT' => cache }, 'rake', 'sdl', 'mruby')
  end

  unless ok
    error 'Failed to build the native dependencies. See the output above.', spaced: true
    exit 1
  end

  # Stamp the build so `ruby2d build` and extconf only link it against a matching
  # gem version — a later upgrade may pin newer SDL/mruby.
  File.write(stamp, Ruby2D::VERSION)

  setup_step 'Built the native dependencies'
  puts "\n    #{"Native (#{target})".bold}"
  setup_list_artifacts(cache_platform)
  puts "\n  Built to #{setup_tildify(cache_platform)}"

  # Build (or rebuild) the CRuby native extension through RubyGems' standard
  # path now that the libraries exist — extconf resolves this cache build. This
  # keeps the extension entirely on RubyGems' rails; setup builds no extension
  # itself.
  setup_step 'Building the Ruby 2D extension'
  puts "    #{'$ gem pristine ruby2d'.dim}\n\n"
  if system('gem', 'pristine', 'ruby2d')
    puts "\n  #{'Ready!'.bold} #{"require 'ruby2d'".bold} now works — run #{'ruby2d build --native <app>.rb'.bold} to build an app.\n\n"
  else
    warning "Couldn't rebuild the extension automatically.", spaced: true
    puts "  Run #{'gem pristine ruby2d'.bold} yourself to finish.\n\n"
  end
end

#setup_clean(yes: false) ⇒ Object

Remove this platform's cache build: the libraries, shared headers, and the regenerable intermediates and downloaded sources.



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
# File 'lib/ruby2d/cli/setup.rb', line 132

def setup_clean(yes: false)
  cache = AssetsTarget.cache_root
  platform = AssetsTarget.platform_dir(root: cache)

  # Nothing cached for this platform — say so rather than confirming a no-op.
  unless Dir.exist?(platform)
    setup_step 'Nothing to remove'
    puts "    #{"no cached dependencies for #{setup_platform_label}".dim}"
    puts "\n  #{setup_tildify(platform)} doesn't exist.\n\n"
    return
  end

  setup_step 'Remove cached dependencies'
  puts "    #{"for #{setup_platform_label}".dim}"
  puts "\n  This removes #{setup_tildify(platform)} and its build files."
  puts "  Rebuilding them with #{'ruby2d setup'.bold} takes several minutes."
  setup_confirm!(yes: yes)

  [platform,
   AssetsTarget.include_dir(root: cache),
   AssetsTarget.build_dir(root: cache),
   AssetsTarget.sources_dir(root: cache)].each { |dir| FileUtils.rm_rf dir }

  puts "\n  Removed #{setup_tildify(platform)}.\n\n"
end

#setup_command?(cmd) ⇒ Boolean

Whether a command is on PATH. Unix only — setup's preflight runs there; on Windows the assets Rakefile bootstraps git/cmake via MSYS2 pacman itself.

Returns:

  • (Boolean)


45
46
47
# File 'lib/ruby2d/cli/setup.rb', line 45

def setup_command?(cmd)
  system("command -v #{cmd} >/dev/null 2>&1")
end

#setup_confirm!(yes:) ⇒ Object

Ask before doing the involved work, after printing what it entails. Enter accepts (the user asked for this by typing the command — the prompt is there to surface the cost, not to second-guess). Skipped by --yes, and when stdin isn't a TTY (CI, pipelines) where there's nobody to answer.



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ruby2d/cli/setup.rb', line 109

def setup_confirm!(yes:)
  return if yes || !$stdin.tty?

  print "\n  Continue? #{'[Y/n]'.dim} "
  # A bare Enter accepts; EOF (Ctrl-D) does not — closing the input is a way
  # out of the prompt, not a way to start a multi-minute build.
  answer = $stdin.gets
  return if answer && (answer.strip.empty? || %w[y yes].include?(answer.strip.downcase))

  puts "\n  Cancelled.\n\n"
  exit 0
end

#setup_list_artifacts(dir) ⇒ Object

List the static libraries and binaries a build produced under dir.



124
125
126
127
# File 'lib/ruby2d/cli/setup.rb', line 124

def setup_list_artifacts(dir)
  Dir.glob(File.join(dir, 'lib', '*.a')).map { |f| File.basename(f) }.sort.each { |f| puts "    lib/#{f}" }
  Dir.glob(File.join(dir, 'bin', '*')).map { |f| File.basename(f) }.sort.each { |f| puts "    bin/#{f}" }
end

#setup_pkg_config?(mod) ⇒ Boolean

Whether a pkg-config module's development files are installed. Used by the Linux preflight to find missing display libraries before SDL's CMake aborts on them — a wall of text several seconds into the build.

Returns:

  • (Boolean)


53
54
55
# File 'lib/ruby2d/cli/setup.rb', line 53

def setup_pkg_config?(mod)
  system("pkg-config --exists #{mod} >/dev/null 2>&1")
end

#setup_platform_labelObject

A friendly label for the platform being built (e.g. "macOS (x86_64)"). Reads the resolved target rather than the host: RUBY2D_ASSETS_OS/ARCH redirect setup at another platform's directory, and naming the host would then describe a build that isn't happening. Any toolchain is left to the target id printed alongside it.



28
29
30
31
32
33
# File 'lib/ruby2d/cli/setup.rb', line 28

def setup_platform_label
  os, arch, = AssetsTarget.resolved_target
  name = { 'macos' => 'macOS', 'windows' => 'Windows',
           'linux' => 'Linux', 'bsd' => 'BSD' }.fetch(os, os)
  "#{name} (#{arch})"
end

#setup_preflight!Object

Check the host build toolchain up front and, if something's missing, print a platform-specific hint and stop — rather than failing partway through a multi-minute build. No-op on Windows, where the assets Rakefile offers to install git/cmake via pacman during its own preflight.



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
# File 'lib/ruby2d/cli/setup.rb', line 62

def setup_preflight!
  return if AssetsTarget.host_os == 'windows'

  cc = ENV['CC'] || 'cc'
  linux = AssetsTarget.host_os == 'linux'
  missing = []
  missing << 'git'   unless setup_command?('git')
  missing << 'cmake' unless setup_command?('cmake')
  missing << cc      unless setup_command?(cc)
  # SDL's Linux build finds Wayland (and much else) through pkg-config, and the
  # display-library check below needs it to answer — so require it here.
  missing << 'pkg-config' if linux && !setup_command?('pkg-config')

  unless missing.empty?
    hint = case AssetsTarget.host_os
           when 'macos'
             'Install the Xcode Command Line Tools (`xcode-select --install`) and CMake (`brew install cmake`).'
           when 'linux'
             'Install a C toolchain, CMake, Git, and pkg-config — e.g. `sudo apt install build-essential cmake git pkg-config`.'
           else
             'Install a C toolchain, CMake, and Git with your system package manager (e.g. `pkg install cmake git`).'
           end

    error "Missing build tools: #{missing.join(', ')}", spaced: true
    puts "  #{hint}"
    exit 1
  end

  # SDL won't build a desktop windowing backend without X11 or Wayland
  # development libraries; its CMake mirrors this exact check, but only after
  # cloning and configuring — so catch it here with a clean, actionable message
  # instead of that error's wall of text. pkg-config is guaranteed present
  # above, so it can answer for both. Linux only: the fix (apt packages, the
  # SDL wiki) is Debian-shaped, and BSD's expert audience is served by CMake's.
  if linux && !setup_pkg_config?('x11') && !setup_pkg_config?('wayland-client')
    error 'SDL needs X11 or Wayland development libraries; neither was found.', spaced: true
    puts "  Install SDL's build dependencies for your distribution, then re-run #{'ruby2d setup'.bold}:"
    puts '  https://wiki.libsdl.org/SDL3/README-linux'
    exit 1
  end
end

#setup_step(title) ⇒ Object

A ruby-red diamond banner, matching the ruby2d CLI and rake output.



18
19
20
# File 'lib/ruby2d/cli/setup.rb', line 18

def setup_step(title)
  puts "\n  #{''.ruby2d_red} #{title.bold}"
end

#setup_tildify(path) ⇒ Object

Abbreviate a home-relative path to ~/… for tidy, portable output.



37
38
39
40
# File 'lib/ruby2d/cli/setup.rb', line 37

def setup_tildify(path)
  home = Dir.home
  path.start_with?(home) ? path.sub(home, '~') : path
end

#shell_escape(path) ⇒ Object

Shell-escape a path for safe interpolation into a command string, so a gem or asset install location containing spaces (or other shell metacharacters) doesn't break the cc/emcc/mrbc invocations. A no-op for ordinary paths.



22
23
24
# File 'lib/ruby2d/cli/build.rb', line 22

def shell_escape(path)
  Shellwords.escape(path.to_s)
end

#skip_build(reason) ⇒ Object

Install without the native extension: write a do-nothing Makefile so gem install still completes, surface the recovery guidance, and exit cleanly. The user finishes with ruby2d setup, or a package-manager install + gem pristine ruby2d. Far friendlier than aborting the whole install — and the ruby2d CLI (including setup) keeps working without the extension.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'ext/ruby2d/extconf.rb', line 35

def skip_build(reason)
  notice = "Ruby 2D: #{reason}\n#{Ruby2D::DepsHelp.notice}"

  # RubyGems runs extconf via Open3.popen2e and hides the captured stdout/stderr
  # on a successful (exit-0) install, so a plain `puts` never reaches the user.
  # Write straight to the controlling terminal — which popen2e leaves attached —
  # to surface the guidance on an interactive install. Best-effort: with no tty
  # (CI, piped) it's skipped, and the same guidance still shows at run time via
  # the LoadError rescue in lib/ruby2d/core.rb.
  begin
    File.open('/dev/tty', 'w') { |tty| tty.puts notice }
  rescue SystemCallError
    # no controlling terminal — rely on the runtime notice
  end
  puts notice # also captured to the build log (visible with `gem install --verbose`)

  File.write('Makefile', "all:\ninstall:\nclean:\n")
  exit 0
end

#static_libs_present?(lib_dir) ⇒ Boolean

Returns:

  • (Boolean)


18
19
20
# File 'ext/ruby2d/extconf.rb', line 18

def static_libs_present?(lib_dir)
  SDL3_STATIC_LIBS.all? { |lib| File.exist?(File.join(lib_dir, lib)) }
end

#step(title) ⇒ Object

Print a build-step banner — a ruby-red diamond and a bold title — to match the ruby2d CLI and rake output. Artifact paths are listed beneath it.



42
43
44
# File 'lib/ruby2d/cli/build.rb', line 42

def step(title)
  puts "\n  #{''.ruby2d_red} #{title.bold}"
end

#strip_require(file) ⇒ Object

Neutralize require 'ruby2d' / require 'ruby2d/core' lines — the bundled build already provides Ruby2D. Blank each one out rather than deleting it, so every following line keeps its original number and an mrbc compile error still points at the right line in the user's source.



68
69
70
71
72
# File 'lib/ruby2d/cli/build.rb', line 68

def strip_require(file)
  File.foreach(file).map do |line|
    line.match?(/require ('|")ruby2d(\/core)?('|")/) ? "\n" : line
  end.join
end

#warning(msg, spaced: false, indent: 0) ⇒ Object



27
28
29
# File 'lib/ruby2d/cli/messages.rb', line 27

def warning(msg, spaced: false, indent: 0)
  puts ruby2d_labeled('Warning:'.warning, msg, spaced, indent)
end

#wrote(path) ⇒ Object

List a file or bundle the build produced, beneath its step banner. The dim wrote label marks it as an output without competing with the path itself.



49
50
51
# File 'lib/ruby2d/cli/build.rb', line 49

def wrote(path)
  puts "    #{'wrote'.dim} #{path}"
end