Class: AMS::ExtensionManager

Inherits:
Object
  • Object
show all
Defined in:
RubyExtension/ams_Lib/extension_manager.rb

Overview

An extension manager for copying, loading, and removing dynamic linked libraries, C extensions, and rubies. Thanks to ThomThom for allowing me to extend his original C extension manager.

Since:

  • 3.5.0

Instance Method Summary collapse

Constructor Details

#initialize(ext_path, ext_version, experimental = false) ⇒ ExtensionManager

Note:

When the libraries are copied, they are obtained from EXT_PATH/libraries/stage/.

Note:

When the libraries are loaded, they are required from EXT_PATH/libraries/VERSION/ or TEMP_EXT_PATH/libraries/VERSION/ or EXT_PATH/libraries/stage/ (as the last resort).

Note:

Rubies are loaded from EXT_PATH.

Apply a staging technique to the dynamically linked libraries, to bypass overwrite issues when updating the loaded extension.

  1. The files within EXT_PATH/libraries/stage/ folder are used as resources for copying and are usually not loaded. These files are the ones that get overwritten when the extension is updated.

  2. Unless already created, the manager creates an additional version-specific folder within EXT_PATH/libraries/ and copies the platform-specific, staged resources into the folder. The libraries within the version-specific folder are the ones that get loaded.

  3. In case the extension is installed outside the user directory, where the file permissions are limited, the necessary staged resources are copied into a TEMP folder and are loaded from there.

  4. If all fails, the last resort is loading from the staged directory.

  5. An optional clean-up method removes outdated version-specific folders, and unregistered rubies, for the purpose of keeping the library directory clean.

Examples:

dir = File.dirname(__FILE__)
base = File.basename(__FILE__)
ext_manager = AMS::ExtensionManager.new(dir, "1.2.3")
ext_manager.add_c_extension('my_lib')
ext_manager.add_ruby('some_ruby_file_name')
ext_manager.add_ruby('some_other_ruby_file_name')
ext_manager.add_ruby_no_require(base)
ext_manager.require_all
ext_manager.clean_up(true)

Parameters:

  • ext_path (String)

    A path to the extension.

  • ext_version (String)

    A version of the extension.

  • experimental (Boolean) (defaults to: false)

    The experimental option updates the libraries every time they are loaded. This is useful if the library is in development.

Since:

  • 3.5.0


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 52

def initialize(ext_path, ext_version, experimental = false)
  @ext_path = ext_path.to_s.dup
  @ext_version = ext_version.to_s.dup
  @experimental = experimental ? true : false
  unless AMS::IS_RUBY_VERSION_18
    @ext_path.force_encoding('UTF-8')
    @ext_version.force_encoding('UTF-8')
  end
  if @ext_version.empty? || @ext_version.gsub(/[^A-Za-z0-9\.\-]/i, '') != @ext_version
    raise(IOError, "The given extension version, \"#{@ext_version}\", is invalid!")
  end
  unless ::File.directory?(@ext_path)
    raise(IOError, "The given extension path, \"#{@ext_path}\", is invalid!")
  end
  @ext_name = ::File.basename(@ext_path)
  @c_extensions = []
  @libraries = []
  @rubies = []
  @rubies_no_require = []
end

Instance Method Details

#add_c_extension(filename) ⇒ void

Note:

All C extension files are be copied from EXT_PATH/libraries/stage/PLATFORM + BIT/RUBY_VERSION/.

This method returns an undefined value.

Add a C extension file that must be copied/loaded.

Parameters:

  • filename (String)

    Library filename.

Since:

  • 3.5.0


78
79
80
81
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 78

def add_c_extension(filename)
  filename = File.basename(filename).gsub(/\.(so|bundle)$/, '')
  @c_extensions << filename
end

#add_optional_library(filename) ⇒ void

Note:

All dll/dylib files are copied from EXT_PATH/libraries/stage/PLATFORM + BIT/.

This method returns an undefined value.

Add a library file that will be copied/loaded in case it's there.

Parameters:

  • filename (String)

    Library filename.

Since:

  • 3.5.0


99
100
101
102
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 99

def add_optional_library(filename)
  filename = File.basename(filename).gsub(/\.(dll|dylib)$/, '')
  @libraries << [filename, false]
end

#add_required_library(filename) ⇒ void

Note:

All dll/dylib files are copied from EXT_PATH/libraries/stage/PLATFORM + BIT/.

This method returns an undefined value.

Add a library file that must be copied/loaded.

Parameters:

  • filename (String)

    Library filename.

Since:

  • 3.5.0


88
89
90
91
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 88

def add_required_library(filename)
  filename = File.basename(filename).gsub(/\.(dll|dylib)$/, '')
  @libraries << [filename, true]
end

#add_ruby(filename, only_register = false) ⇒ Object

Note:

All added rubies are loaded last in the order they are added.

Add a Ruby file that will be required and ignored from cleanup.

Parameters:

  • filename (String)

    Ruby filename.

Since:

  • 3.5.0


107
108
109
110
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 107

def add_ruby(filename, only_register = false)
  filename = File.basename(filename).gsub(/\.(rb|rbs|rbe)$/, '')
  @rubies << filename
end

#add_ruby_no_require(filename) ⇒ Object

Add a Ruby file that will be ignored from cleanup but not required.

Parameters:

  • filename (String)

    Ruby filename.

Since:

  • 3.5.0


114
115
116
117
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 114

def add_ruby_no_require(filename)
  filename = File.basename(filename).gsub(/\.(rb|rbs|rbe)$/, '')
  @rubies_no_require << filename
end

#clean_up(clean_rubies = false) ⇒ void

Note:

This does not erase any files from the TEMP folder, as they may be used by other SketchUp versions.

Note:

This does not raise any errors upon failure to delete a file or a directory.

This method returns an undefined value.

Erase all unused libraries, c extension files, and unregistered ruby files (if clean_rubies is enabled).

Parameters:

  • clean_rubies (Boolean) (defaults to: false)

    Whether to delete all unregistered Ruby files within EXT_PATH.

Since:

  • 3.5.0


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
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 373

def clean_up(clean_rubies = false)
  lib_load_dir = ::File.join(@ext_path, 'libraries')
  lib_load_dir.force_encoding("UTF-8") unless AMS::IS_RUBY_VERSION_18
  if ::File.directory?(lib_load_dir)
    to_skip = ['.', '..', 'stage', @ext_version]
    ::Dir.entries(lib_load_dir).each { |entry|
      next if to_skip.include?(entry)
      entry_fpath = ::File.join(lib_load_dir, entry)
      entry_fpath.force_encoding("UTF-8") unless AMS::IS_RUBY_VERSION_18
      begin
        FileUtils.rm_r(entry_fpath)
      rescue Exception => err
        # Do nothing
      end
    }
  end
  if clean_rubies
    ::Dir.glob( ::File.join(@ext_path, '*.{rb, rbs, rbe}') ).each { |fpath|
      fpath.force_encoding("UTF-8") unless AMS::IS_RUBY_VERSION_18
      basename = ::File.basename(fpath, '.*')
      next if @rubies.include?(basename) || @rubies_no_require.include?(basename)
      begin
        ::File.delete(fpath)
      rescue Exception => err
        # Do nothing
      end
    }
  end
end

#require_allvoid

Note:

All .dll files are loaded first, in the order they are added, all .so or .bundle files afterwards, and all ruby files last. All .dylib files are expected to be loaded by a C extension.

This method returns an undefined value.

Copy and load all the required and optional files.

Raises:

  • (IOError, LoadError)

    if a required file doesn't exist.

Since:

  • 3.5.0


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
# File 'RubyExtension/ams_Lib/extension_manager.rb', line 125

def require_all
  ops = AMS::IS_PLATFORM_WINDOWS ? 'win' : 'osx'
  bit = AMS::IS_SKETCHUP_64BIT ? '64' : '32'
  rbv = RUBY_VERSION[0..2].to_s
  c_ext = AMS::IS_PLATFORM_WINDOWS ? '.so' : '.bundle'
  l_ext = AMS::IS_PLATFORM_WINDOWS ? '.dll' : '.dylib'

  stage_path = ::File.join(@ext_path, 'libraries', 'stage')
  stage_lib_path = ::File.join(stage_path, ops+bit)
  stage_ext_path = ::File.join(stage_lib_path, rbv)

  version_path = ::File.join(@ext_path, 'libraries', @ext_version)
  version_lib_path = ::File.join(version_path, ops+bit)
  version_ext_path = ::File.join(version_lib_path, rbv)

  temp_version_path = ::File.join(AMS::TEMP_DIR, @ext_name, 'libraries', @ext_version)
  temp_version_lib_path = ::File.join(temp_version_path, ops+bit)
  temp_version_ext_path = ::File.join(temp_version_lib_path, rbv)

  load_path_id = 0 # 0 - stage_path, 1 - version_path, 2 - temp_version_path

  # Check if all the required files exist at version_path
  libraries_exist = true
  @libraries.each { |data|
    next unless data[1]
    fpath = ::File.join(version_lib_path, data[0] + l_ext)
    unless ::File.exists?(fpath)
      libraries_exist = false
      break
    end
  }
  if libraries_exist
    @c_extensions.each { |filename|
      fpath = ::File.join(version_ext_path, filename + c_ext)
      unless ::File.exists?(fpath)
        libraries_exist = false
        break
      end
    }
  end
  load_path_id = 1 if libraries_exist
  # If not, check if all the required files exist at temp_version_path
  unless libraries_exist
    libraries_exist = true
    @libraries.each { |data|
      next unless data[1]
      fpath = ::File.join(temp_version_lib_path, data[0] + l_ext)
      if ::File.exists?(fpath)
        libraries_exist = false
        break
      end
    }
    if libraries_exist
      @c_extensions.each { |filename|
        fpath = ::File.join(temp_version_ext_path, filename + c_ext)
        unless ::File.exists?(fpath)
          libraries_exist = false
          break
        end
      }
    end
    load_path_id = 2 if libraries_exist
  end
  # If not or experimental mode is turned on
  if !libraries_exist || @experimental
    # First verify that all the required stage files exist
    unless ::File.directory?(stage_path)
      raise(IOError, "Stage directory, \"#{stage_path}\", is missing!")
    end
    @libraries.each { |data|
      next unless data[1]
      fpath = ::File.join(stage_lib_path, data[0] + l_ext)
      unless ::File.exists?(fpath)
        raise(IOError, "The required, staged library file, \"#{fpath}\", is missing!")
      end
    }
    @c_extensions.each { |filename|
      fpath = ::File.join(stage_ext_path, filename + c_ext)
      unless ::File.exists?(fpath)
        raise(IOError, "The required, staged c extension file, \"#{fpath}\", is missing!")
      end
    }
    # Create directory to version_ext_path
    copying_success = true
    unless ::File.directory?(version_ext_path)
      begin
        FileUtils.mkdir_p(version_ext_path)
      rescue Exception => err
        copying_success = false
      end
    end
    # If the directory to version_ext_path was successfully created
    if copying_success
      # Attempt to copy all the library stage files to version_lib_path
      @libraries.each { |data|
        fname = data[0] + l_ext
        src_path = ::File.join(stage_lib_path, fname)
        dst_path = ::File.join(version_lib_path, fname)
        # Skip if the file is already copied; or overwrite with a newer version.
        #~ next if ::File.exists?(dst_path)
        # Skip if the file is optional and doesn't exist at stage.
        # If the file is required and doesn't exist at stage,
        # the IOError is raised in code above.
        next if !data[1] && !::File.exists?(src_path)
        # Otherwise copy from stage_lib_path to version_lib_path.
        begin
          FileUtils.copy_file(src_path, dst_path)
        rescue Exception => err
          copying_success = false
          break
        end
      }
    end
    # If the required libraries were successfully copied to version_lib_path
    if copying_success
      # Attempt to copy all the c extension stage files to version_ext_path
      @c_extensions.each { |filename|
        fname = filename + c_ext
        src_path = ::File.join(stage_ext_path, fname)
        dst_path = ::File.join(version_ext_path, fname)
        # Copy from stage_ext_path to version_ext_path.
        begin
          FileUtils.copy_file(src_path, dst_path)
        rescue Exception => err
          copying_success = false
          break
        end
      }
    end
    # If all libraries and c extension files were copied successfully to version_path
    if copying_success
      # Set load path to version_path
      load_path_id = 1
    else
      # Create directory to temp_version_ext_path
      copying_success = true
      unless ::File.directory?(temp_version_ext_path)
        begin
          FileUtils.mkdir_p(temp_version_ext_path)
        rescue Exception => err
          copying_success = false
        end
      end
      if copying_success
        # Attempt to copy all the library stage files to the temp_version_lib_path.
        @libraries.each { |data|
          fname = data[0] + l_ext
          src_path = ::File.join(stage_lib_path, fname)
          dst_path = ::File.join(temp_version_lib_path, fname)
          # Skip if the file is already copied; or overwrite with a newer version.
          #~ next if ::File.exists?(dst_path)
          # Skip if the file is optional and doesn't exist at stage.
          # If the file is required and doesn't exist at stage,
          # the IOError is raised in code above.
          next if !data[1] && !::File.exists?(src_path)
          # Copy from stage_lib_path to temp_version_lib_path
          begin
            FileUtils.copy_file(src_path, dst_path)
          rescue Exception => err
            copying_success = false
            break
          end
        }
      end
      # If the required libraries were successfully copied to version_lib_path
      if copying_success
        # Attempt to copy all the c extension stage files to version_ext_path
        @c_extensions.each { |filename|
          fname = filename + c_ext
          src_path = ::File.join(stage_ext_path, fname)
          dst_path = ::File.join(temp_version_ext_path, fname)
          # Copy from stage_ext_path to temp_version_ext_path
          begin
            FileUtils.copy_file(src_path, dst_path)
          rescue Exception => err
            copying_success = false
            break
          end
        }
      end
      # If all successfully copied
      if copying_success
        # Set load_path_id to temp_version_path
        load_path_id = 2
      else
        # Otherwise, set load_path_id to stage_path as the last resort
        load_path_id = 0
      end
    end
  end
  # Convert load_path_id to the right paths
  if load_path_id == 0
    lib_load_path = stage_lib_path
    ext_load_path = stage_ext_path
  elsif load_path_id == 1
    lib_load_path = version_lib_path
    ext_load_path = version_ext_path
  else
    lib_load_path = temp_version_lib_path
    ext_load_path = temp_version_ext_path
  end
  # Load all libraries in given order
  dll_report = nil
  if !@libraries.empty? && AMS::IS_PLATFORM_WINDOWS
    dll_report = "DLL Report:\n"
    @libraries.each { |data|
      fname = data[0] + l_ext
      fpath = ::File.join(lib_load_path, fname)
      fpath.force_encoding('UTF-8') unless AMS::IS_RUBY_VERSION_18
      if ::File.exists?(fpath)
        dll_report << sprintf("%s : %d\n", fname, AMS::DLL.load_library(fpath))
      else
        dll_report << sprintf("%s : missing\n", fname)
      end
    }
  end
  # Load all c extension in given order
  @c_extensions.each { |filename|
    fname = filename + c_ext
    fpath = ::File.join(ext_load_path, fname)
    if ::File.exists?(fpath)
      begin
        ::Kernel.require(fpath)
      rescue LoadError => e
        msg = "An exception occurred while loading #{@ext_name}, version #{@ext_version}!\n\n#{e.message}"
        msg << "\n\n#{dll_report}" if dll_report
        raise(e.class, msg, caller)
      end
    else
      raise(IOError, "The required c extension file, \"#{fpath}\", is missing!")
    end
  }
  # Require all the rubies in given order
  @rubies.each { |filename|
    fpath = ::File.join(@ext_path, filename)
    ::Sketchup.require(fpath)
  }
end