assimp-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "rbconfig"
5
+ require "thread"
6
+ require_relative "native/platform"
7
+ require_relative "native/types"
8
+
9
+ module Assimp
10
+ module Native
11
+ extend FFI::Library
12
+
13
+ ENVIRONMENT_KEY = "ASSIMP_LIBRARY_PATH"
14
+ REQUIRED_MAJOR = 5
15
+ LOAD_MUTEX = Mutex.new
16
+
17
+ class << self
18
+ def load!
19
+ return self if @loaded
20
+
21
+ LOAD_MUTEX.synchronize do
22
+ return self if @loaded
23
+
24
+ ffi_lib(resolve_library)
25
+ attach_api
26
+ verify_version!
27
+ @loaded = true
28
+ end
29
+ self
30
+ rescue FFI::NotFoundError, LoadError => e
31
+ raise LibraryError, "could not load Assimp 5.x: #{e.message}"
32
+ end
33
+
34
+ def loaded?
35
+ @loaded == true
36
+ end
37
+
38
+ def version
39
+ load!
40
+ [aiGetVersionMajor, aiGetVersionMinor, @version_patch ? aiGetVersionPatch : 0]
41
+ end
42
+
43
+ def revision
44
+ load!
45
+ aiGetVersionRevision
46
+ end
47
+
48
+ def supports_extension?(extension)
49
+ load!
50
+ aiIsExtensionSupported(normalize_extension(extension)) != 0
51
+ end
52
+
53
+ def extension_list
54
+ load!
55
+ output = StringValue.new
56
+ aiGetExtensionList(output)
57
+ output.to_s.split(";").map { |extension| extension.delete_prefix("*.") }.freeze
58
+ end
59
+
60
+ def library_candidates
61
+ explicit = ENV[ENVIRONMENT_KEY]
62
+ return [File.expand_path(explicit)] if explicit && !explicit.empty?
63
+
64
+ packaged = packaged_library_candidates.select { |path| File.file?(path) }
65
+ packaged + platform_library_names
66
+ end
67
+
68
+ private
69
+
70
+ def resolve_library
71
+ explicit = ENV[ENVIRONMENT_KEY]
72
+ if explicit && !File.file?(File.expand_path(explicit))
73
+ raise LoadError, "#{ENVIRONMENT_KEY} does not point to a file: #{File.expand_path(explicit)}"
74
+ end
75
+
76
+ library_candidates.find { |candidate| loadable?(candidate) } ||
77
+ raise(LoadError, missing_library_message)
78
+ end
79
+
80
+ def loadable?(candidate)
81
+ flags = FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_LOCAL
82
+ FFI::DynamicLibrary.open(candidate, flags)
83
+ true
84
+ rescue LoadError
85
+ false
86
+ end
87
+
88
+ def packaged_library_candidates
89
+ platform = Platform.tag
90
+ roots = $LOAD_PATH.map { |path| File.join(path, "assimp", "native") }
91
+ roots.flat_map do |root|
92
+ Platform.library_names.flat_map do |name|
93
+ [File.join(root, platform, name), File.join(root, name)]
94
+ end
95
+ end
96
+ end
97
+
98
+ def platform_library_names
99
+ ["assimp", *Platform.library_names]
100
+ end
101
+
102
+ def attach_api
103
+ attach_function :aiImportFile, %i[string uint], :pointer, blocking: true
104
+ attach_function :aiImportFileFromMemory, %i[pointer uint uint string], :pointer, blocking: true
105
+ attach_function :aiReleaseImport, [:pointer], :void
106
+ attach_function :aiGetErrorString, [], :string
107
+ attach_function :aiGetVersionMajor, [], :uint
108
+ attach_function :aiGetVersionMinor, [], :uint
109
+ attach_function :aiGetVersionRevision, [], :uint
110
+ @version_patch = attach_optional_function(:aiGetVersionPatch, [], :uint)
111
+ attach_function :aiIsExtensionSupported, [:string], :int
112
+ attach_function :aiGetExtensionList, [:pointer], :void
113
+
114
+ attach_function :aiGetMaterialProperty, %i[pointer string uint uint pointer], :int
115
+ attach_function :aiGetMaterialColor, %i[pointer string uint uint pointer], :int
116
+ attach_function :aiGetMaterialFloatArray, %i[pointer string uint uint pointer pointer], :int
117
+ attach_function :aiGetMaterialIntegerArray, %i[pointer string uint uint pointer pointer], :int
118
+ attach_function :aiGetMaterialString, %i[pointer string uint uint pointer], :int
119
+ attach_function :aiGetMaterialTextureCount, %i[pointer int], :uint
120
+ attach_function :aiGetMaterialTexture,
121
+ %i[pointer int uint pointer pointer pointer pointer pointer pointer pointer], :int
122
+ end
123
+
124
+ def attach_optional_function(name, arguments, result)
125
+ attach_function(name, arguments, result)
126
+ true
127
+ rescue FFI::NotFoundError
128
+ false
129
+ end
130
+
131
+ def normalize_extension(extension)
132
+ value = String(extension)
133
+ value.start_with?(".") ? value : ".#{value}"
134
+ end
135
+
136
+ def verify_version!
137
+ major = aiGetVersionMajor
138
+ return if major == REQUIRED_MAJOR
139
+
140
+ raise IncompatibleLibraryError, "Assimp #{REQUIRED_MAJOR}.x is required, found #{major}.x"
141
+ end
142
+
143
+ def missing_library_message
144
+ "set #{ENVIRONMENT_KEY} or install a compatible libassimp shared library"
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ module NativeReader
5
+ module_function
6
+
7
+ def pointers(pointer, count)
8
+ return [] if count.zero?
9
+ raise ImportError, "native pointer array is null" if pointer.null?
10
+
11
+ pointer.read_array_of_pointer(count)
12
+ end
13
+
14
+ def struct_at(pointer, type, index)
15
+ type.new(pointer + (type.size * index))
16
+ end
17
+
18
+ def bytes(pointer, size)
19
+ return "".b.freeze if size.zero?
20
+ raise ImportError, "native data pointer is null" if pointer.null?
21
+
22
+ pointer.read_bytes(size).force_encoding(Encoding::BINARY).freeze
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class Node
5
+ attr_reader :name, :matrix, :children, :mesh_indices
6
+
7
+ def initialize(name:, matrix:, children:, mesh_indices:)
8
+ @name = String(name).dup.freeze
9
+ @matrix = matrix.map { |value| Float(value) }.freeze
10
+ raise ArgumentError, "node matrix must contain 16 values" unless @matrix.length == 16
11
+
12
+ @children = children.freeze
13
+ @mesh_indices = mesh_indices.map { |index| Integer(index) }.freeze
14
+ freeze
15
+ end
16
+
17
+ def find(name)
18
+ target = String(name)
19
+ stack = [self]
20
+ until stack.empty?
21
+ node = stack.pop
22
+ return node if node.name == target
23
+
24
+ stack.concat(node.children.reverse)
25
+ end
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "native/generated"
4
+
5
+ module Assimp
6
+ POST_PROCESS = Native::POST_PROCESS
7
+
8
+ PROCESS_PRESETS = {
9
+ none: [].freeze,
10
+ fast: %i[triangulate join_identical_vertices gen_normals].freeze,
11
+ realtime: %i[
12
+ triangulate join_identical_vertices gen_smooth_normals calc_tangent_space
13
+ limit_bone_weights improve_cache_locality sort_by_p_type
14
+ ].freeze
15
+ }.freeze
16
+
17
+ module PostProcess
18
+ module_function
19
+
20
+ def flags(value)
21
+ names = value.is_a?(Symbol) ? preset(value) : Array(value)
22
+ names.reduce(0) do |result, name|
23
+ result | POST_PROCESS.fetch(name.to_sym) do
24
+ raise ArgumentError, "unknown post-process flag #{name.inspect}"
25
+ end
26
+ end
27
+ end
28
+
29
+ def preset(name)
30
+ PROCESS_PRESETS.fetch(name) do
31
+ raise ArgumentError, "unknown post-process preset #{name.inspect}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class RawScene
5
+ class View
6
+ attr_reader :native_type
7
+
8
+ def initialize(owner, native_type, pointer)
9
+ @owner = owner
10
+ @native_type = native_type
11
+ @pointer = pointer
12
+ end
13
+
14
+ def [](field)
15
+ native[field]
16
+ end
17
+
18
+ def to_ptr
19
+ ensure_valid!
20
+ @pointer
21
+ end
22
+
23
+ def inspect
24
+ state = @owner.valid? ? "valid" : "released"
25
+ "#<#{self.class.name} #{@native_type.name} #{state}>"
26
+ end
27
+
28
+ private
29
+
30
+ def native
31
+ ensure_valid!
32
+ @native_type.new(@pointer)
33
+ end
34
+
35
+ def ensure_valid!
36
+ return if @owner.valid?
37
+
38
+ raise ReleasedError, "raw view is only valid inside the Assimp.open block"
39
+ end
40
+ end
41
+
42
+ def initialize(pointer)
43
+ @pointer = pointer
44
+ @valid = true
45
+ end
46
+
47
+ def valid?
48
+ @valid
49
+ end
50
+
51
+ def native
52
+ ensure_valid!
53
+ View.new(self, Native::Scene, @pointer)
54
+ end
55
+
56
+ def pointer
57
+ ensure_valid!
58
+ @pointer
59
+ end
60
+
61
+ def meshes
62
+ source = native_struct
63
+ NativeReader.pointers(source[:meshes], source[:num_meshes]).map do |item|
64
+ View.new(self, Native::Mesh, item)
65
+ end
66
+ end
67
+
68
+ def materials
69
+ source = native_struct
70
+ NativeReader.pointers(source[:materials], source[:num_materials]).map do |item|
71
+ View.new(self, Native::Material, item)
72
+ end
73
+ end
74
+
75
+ def root
76
+ node_pointer = native_struct[:root_node]
77
+ node_pointer.null? ? nil : View.new(self, Native::Node, node_pointer)
78
+ end
79
+
80
+ def invalidate!
81
+ @valid = false
82
+ @pointer = nil
83
+ self
84
+ end
85
+
86
+ private
87
+
88
+ def native_struct
89
+ ensure_valid!
90
+ Native::Scene.new(@pointer)
91
+ end
92
+
93
+ def ensure_valid!
94
+ raise ReleasedError, "raw scene is only valid inside the Assimp.open block" unless valid?
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class Scene
5
+ INCOMPLETE_FLAG = 0x1
6
+
7
+ attr_reader :name, :meshes, :materials, :root, :animations, :textures, :flags
8
+
9
+ def initialize(name:, meshes:, materials:, root:, animations:, textures:, flags:)
10
+ @name = String(name).dup.freeze
11
+ @meshes = meshes.freeze
12
+ @materials = materials.freeze
13
+ @root = root
14
+ @animations = animations.freeze
15
+ @textures = textures.freeze
16
+ @flags = Integer(flags)
17
+ freeze
18
+ end
19
+
20
+ def incomplete?
21
+ (flags & INCOMPLETE_FLAG) != 0
22
+ end
23
+
24
+ def find_node(name)
25
+ root&.find(name)
26
+ end
27
+
28
+ def each_mesh_instance
29
+ return enum_for(__method__) unless block_given?
30
+ return self unless root
31
+
32
+ stack = [[root, Matrix::IDENTITY]]
33
+ until stack.empty?
34
+ node, parent_matrix = stack.pop
35
+ world_matrix = Matrix.multiply(parent_matrix, node.matrix)
36
+ node.mesh_indices.each { |index| yield meshes.fetch(index), world_matrix }
37
+ node.children.reverse_each { |child| stack << [child, world_matrix] }
38
+ end
39
+ self
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class SceneCopier
5
+ def initialize(native: Native)
6
+ @native = native
7
+ @minor_version = native.version[1]
8
+ @mesh_copier = MeshCopier.new(native:)
9
+ @animation_copier = AnimationCopier.new(native:)
10
+ @texture_copier = TextureCopier.new
11
+ end
12
+
13
+ def copy(pointer)
14
+ source = Native::Scene.new(pointer)
15
+ textures = map_pointers(source[:textures], source[:num_textures], @texture_copier)
16
+ material_copier = MaterialCopier.new(native: @native, embedded_textures: textures)
17
+ Scene.new(
18
+ name: @minor_version.zero? ? "" : source[:name].to_s,
19
+ meshes: map_pointers(source[:meshes], source[:num_meshes], @mesh_copier),
20
+ materials: map_pointers(source[:materials], source[:num_materials], material_copier),
21
+ root: copy_node(source[:root_node]),
22
+ animations: map_pointers(source[:animations], source[:num_animations], @animation_copier),
23
+ textures:,
24
+ flags: source[:flags]
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def map_pointers(pointer, count, copier)
31
+ NativeReader.pointers(pointer, count).map { |item| copier.copy(item) }.freeze
32
+ end
33
+
34
+ def copy_node(pointer)
35
+ return nil if pointer.null?
36
+
37
+ source = Native::Node.new(pointer)
38
+ Node.new(
39
+ name: source[:name].to_s,
40
+ matrix: source[:transformation].to_column_major,
41
+ children: NativeReader.pointers(source[:children], source[:num_children]).map { |child| copy_node(child) },
42
+ mesh_indices: source[:num_meshes].zero? ? [] : source[:meshes].read_array_of_uint(source[:num_meshes])
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class TextureCopier
5
+ def copy(pointer)
6
+ source = Native::Texture.new(pointer)
7
+ byte_size = source[:height].zero? ? source[:width] : source[:width] * source[:height] * 4
8
+ EmbeddedTexture.new(
9
+ width: source[:width],
10
+ height: source[:height],
11
+ format_hint: source[:format_hint].to_ptr.read_string(9).delete("\0"),
12
+ filename: source[:filename].to_s,
13
+ bytes: NativeReader.bytes(source[:data], byte_size)
14
+ )
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ VERSION = "1.0.0"
5
+ end
data/lib/assimp.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "assimp/version"
4
+ require_relative "assimp/errors"
5
+ require_relative "assimp/post_process"
6
+ require_relative "assimp/math"
7
+ require_relative "assimp/mesh"
8
+ require_relative "assimp/node"
9
+ require_relative "assimp/material"
10
+ require_relative "assimp/animation"
11
+ require_relative "assimp/embedded_texture"
12
+ require_relative "assimp/scene"
13
+ require_relative "assimp/native"
14
+ require_relative "assimp/native_reader"
15
+ require_relative "assimp/mesh_copier"
16
+ require_relative "assimp/material_copier"
17
+ require_relative "assimp/animation_copier"
18
+ require_relative "assimp/texture_copier"
19
+ require_relative "assimp/scene_copier"
20
+ require_relative "assimp/import_source"
21
+ require_relative "assimp/raw_scene"
22
+ require_relative "assimp/importer"
23
+
24
+ module Assimp
25
+ class << self
26
+ def import(source, process: :realtime, hint: nil)
27
+ Importer.new.import(source, process:, hint:)
28
+ end
29
+
30
+ def open(source, process: :realtime, hint: nil, &block)
31
+ Importer.new.open(source, process:, hint:, &block)
32
+ end
33
+
34
+ def native_version
35
+ Native.version
36
+ end
37
+
38
+ def supports?(extension)
39
+ Native.supports_extension?(extension)
40
+ end
41
+
42
+ def supported_extensions
43
+ Native.extension_list
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "fileutils"
5
+ require "rbconfig"
6
+ require_relative "../lib/assimp/native/platform"
7
+ require_relative "../lib/assimp/version"
8
+
9
+ module AssimpNativePackage
10
+ extend Rake::FileUtilsExt
11
+ module_function
12
+
13
+ ROOT = File.expand_path("..", __dir__)
14
+ ASSIMP_VERSION = "5.4.3"
15
+ PLATFORM = Assimp::Native::Platform.tag
16
+ SOURCE_DIR = File.expand_path(
17
+ ENV.fetch("ASSIMP_SOURCE_DIR", File.join(ROOT, "tmp", "assimp-#{ASSIMP_VERSION}"))
18
+ )
19
+ BUILD_DIR = File.join(ROOT, "tmp", "native", PLATFORM)
20
+
21
+ def explicit_library
22
+ value = ENV["ASSIMP_LIBRARY_PATH"]
23
+ return if value.nil? || value.empty?
24
+
25
+ path = File.expand_path(value)
26
+ raise "ASSIMP_LIBRARY_PATH does not point to a file: #{path}" unless File.file?(path)
27
+
28
+ path
29
+ end
30
+
31
+ def fetch_source
32
+ return if File.file?(File.join(SOURCE_DIR, "CMakeLists.txt"))
33
+ raise "#{SOURCE_DIR} exists but is not an Assimp source tree" if File.exist?(SOURCE_DIR)
34
+
35
+ FileUtils.mkdir_p(File.dirname(SOURCE_DIR))
36
+ sh(
37
+ "git", "clone", "--depth", "1", "--branch", "v#{ASSIMP_VERSION}",
38
+ "https://github.com/assimp/assimp.git", SOURCE_DIR
39
+ )
40
+ end
41
+
42
+ def configure
43
+ fetch_source
44
+ host_os = RbConfig::CONFIG.fetch("host_os")
45
+ bundled_zlib = host_os.match?(/mswin|mingw/) ? "ON" : "OFF"
46
+ command = [
47
+ "cmake",
48
+ "-S", SOURCE_DIR,
49
+ "-B", BUILD_DIR,
50
+ "-DCMAKE_BUILD_TYPE=Release",
51
+ "-DBUILD_SHARED_LIBS=ON",
52
+ "-DASSIMP_BUILD_ASSIMP_TOOLS=OFF",
53
+ "-DASSIMP_BUILD_SAMPLES=OFF",
54
+ "-DASSIMP_BUILD_TESTS=OFF",
55
+ "-DASSIMP_BUILD_DOCS=OFF",
56
+ "-DASSIMP_BUILD_DRACO=OFF",
57
+ "-DASSIMP_BUILD_ZLIB=#{bundled_zlib}",
58
+ "-DASSIMP_WARNINGS_AS_ERRORS=OFF",
59
+ "-DASSIMP_INJECT_DEBUG_POSTFIX=OFF",
60
+ "-DASSIMP_IGNORE_GIT_HASH=ON",
61
+ "-DASSIMP_NO_EXPORT=ON"
62
+ ]
63
+ command << "-DUSE_STATIC_CRT=ON" if host_os.match?(/mswin|mingw/)
64
+ if host_os.include?("darwin")
65
+ command << "-DCMAKE_OSX_ARCHITECTURES=#{RbConfig::CONFIG.fetch("host_cpu")}"
66
+ end
67
+ sh(*command)
68
+ end
69
+
70
+ def compile
71
+ configure
72
+ sh("cmake", "--build", BUILD_DIR, "--config", "Release", "--parallel")
73
+ end
74
+
75
+ def built_library
76
+ names = Assimp::Native::Platform.library_names
77
+ files = Dir.glob(File.join(BUILD_DIR, "**", "*")).select { |path| File.file?(path) }
78
+ match = names.lazy.filter_map do |name|
79
+ files.find { |path| File.basename(path) == name }
80
+ end.first
81
+ match ||= files.find do |path|
82
+ basename = File.basename(path)
83
+ basename.match?(/\Alibassimp(?:\.\d+(?:\.\d+)*)?\.(?:so|dylib)\z/) ||
84
+ basename.match?(/\Aassimp-vc\d+-mt\.dll\z/)
85
+ end
86
+ raise "Assimp shared library was not found in #{BUILD_DIR}" unless match
87
+
88
+ File.realpath(match)
89
+ end
90
+
91
+ def stage
92
+ source = explicit_library
93
+ unless source
94
+ compile
95
+ source = built_library
96
+ end
97
+
98
+ destination = File.join(ROOT, "lib", "assimp", "native", PLATFORM)
99
+ FileUtils.rm_rf(destination)
100
+ FileUtils.mkdir_p(destination)
101
+ FileUtils.cp(
102
+ source,
103
+ File.join(destination, Assimp::Native::Platform.packaged_library_name)
104
+ )
105
+
106
+ license = File.join(SOURCE_DIR, "LICENSE")
107
+ FileUtils.cp(license, File.join(destination, "ASSIMP-LICENSE.txt")) if File.file?(license)
108
+ destination
109
+ end
110
+
111
+ def platform_gem_path
112
+ platform = Gem::Platform.new(PLATFORM)
113
+ File.join(ROOT, "assimp-ruby-#{Assimp::VERSION}-#{platform}.gem")
114
+ end
115
+ end
116
+
117
+ module AssimpPackageSmoke
118
+ extend Rake::FileUtilsExt
119
+ module_function
120
+
121
+ ROOT = File.expand_path("..", __dir__)
122
+
123
+ def run(gem_path, label)
124
+ raise "gem was not built: #{gem_path}" unless File.file?(gem_path)
125
+
126
+ install_dir = File.join(ROOT, "tmp", "gem-smoke", label)
127
+ FileUtils.rm_rf(install_dir)
128
+ FileUtils.mkdir_p(install_dir)
129
+ sh(
130
+ "gem", "install", "--local", "--ignore-dependencies", "--no-document",
131
+ "--install-dir", install_dir, gem_path
132
+ )
133
+
134
+ environment = {
135
+ "GEM_HOME" => install_dir,
136
+ "GEM_PATH" => [install_dir, *Gem.path].uniq.join(File::PATH_SEPARATOR),
137
+ "ASSIMP_RUBY_SMOKE_HOME" => install_dir
138
+ }
139
+ Bundler.with_unbundled_env do
140
+ sh(environment, RbConfig.ruby, File.join(ROOT, "script", "smoke_gem.rb"))
141
+ end
142
+ end
143
+ end
144
+
145
+ namespace :native do
146
+ desc "Configure Assimp #{AssimpNativePackage::ASSIMP_VERSION}"
147
+ task :configure do
148
+ AssimpNativePackage.configure
149
+ end
150
+
151
+ desc "Compile the Assimp shared library"
152
+ task :compile do
153
+ AssimpNativePackage.compile
154
+ end
155
+
156
+ desc "Stage a shared library for the current platform gem"
157
+ task :stage do
158
+ AssimpNativePackage.stage
159
+ end
160
+
161
+ desc "Build a platform gem containing Assimp"
162
+ task gem: :stage do
163
+ Bundler.with_unbundled_env do
164
+ sh({ "ASSIMP_RUBY_PLATFORM_GEM" => "1" }, "gem", "build", "assimp.gemspec")
165
+ end
166
+ end
167
+
168
+ desc "Build, install, and execute the current platform gem"
169
+ task smoke_gem: :gem do
170
+ AssimpPackageSmoke.run(AssimpNativePackage.platform_gem_path, "platform")
171
+ end
172
+ end
173
+
174
+ namespace :package do
175
+ desc "Build the source gem"
176
+ task :source_gem do
177
+ Bundler.with_unbundled_env do
178
+ sh("gem", "build", "assimp.gemspec")
179
+ end
180
+ end
181
+
182
+ desc "Build, install, and execute the source gem against a system Assimp"
183
+ task smoke_source: :source_gem do
184
+ gem_path = File.join(__dir__, "..", "assimp-ruby-#{Assimp::VERSION}.gem")
185
+ AssimpPackageSmoke.run(gem_path, "source")
186
+ end
187
+ end