Sketchup.require failing in local gem after encryption

I am using a ruby gem in an extension. I include a local version of the gem a subfolder, so users don’t have to install it themselves. The gem is pure Ruby, so no problem with binaries.

Prior to doing extension signing/encryption, the following was working in main.rb and I could use the gem methods on any computer:

Sketchup.require('somtum/gems/locale/locale')

Then each file in locale would require whatever it needed using native require calls.

For reference, here is the folder structure:

somtum.rb
somtum/
somtum/main.rb
somtum/gems/
somtum/gems/locale/locale.rb
somtum/gems/locale/locale/version.rb
somtum/gems/locale/locale/middleware.rb
somtum/gems/locale/locale/info.rb
somtum/gems/locale/locale/driver/cgi.rb
somtum/gems/locale/locale/driver/env.rb
somtum/gems/locale/locale/driver/win32_table.rb
somtum/gems/locale/locale/driver/posix.rb
somtum/gems/locale/locale/driver/win32.rb
somtum/gems/locale/locale/taglist.rb
somtum/gems/locale/locale/driver.rb
somtum/gems/locale/locale/tag.rb
somtum/gems/locale/locale/data/languages.tab.gz
somtum/gems/locale/locale/data/regions.tab.gz
somtum/gems/locale/locale/info/language.rb
somtum/gems/locale/locale/info/region.rb
somtum/gems/locale/locale/tag/irregular.rb
somtum/gems/locale/locale/tag/common.rb
somtum/gems/locale/locale/tag/simple.rb
somtum/gems/locale/locale/tag/posix.rb
somtum/gems/locale/locale/tag/rfc.rb
somtum/gems/locale/locale/tag/cldr.rb

After signing/encryption, this fails.

First I tried to replace all the require calls in my gems folder with Sketchup.require

So for instance, locale.rb now contains

Sketchup.require 'locale/tag'
Sketchup.require 'locale/taglist'
Sketchup.require 'locale/driver'
Sketchup.require 'locale/version'

Here is the error

Error Loading File locale/tag
Could not find included file 'locale/tag'
Error Loading File locale/taglist
Could not find included file 'locale/taglist'
Error Loading File locale/driver
Could not find included file 'locale/driver'
Error Loading File locale/version
Could not find included file 'locale/version'
Error Loading File Z:/shared/plug/src/somtum/gems/locale/locale.rb
Error: #<NameError: uninitialized constant Locale::Tag>
Z:/shared/plug/src/somtum/gems/locale/locale.rb:91:in <module:Locale>'
Z:/shared/plug/src/somtum/gems/locale/locale.rb:23:in <top (required)>'
Z:/shared/plug/src/somtum/main.rb:20:in `require'

So it appears that its still failing, and I’m not sure why.

Another option would be to sign/encrypt the extension and then copy over the encrypted gem with an unencrypted gem. However, this breaks the signing mechanism.

Any ideas how I can make the require calls work? Am I doing something obviously wrong?

Sketchup.require ‘locale/tag’
Sketchup.require ‘locale/taglist’
Sketchup.require ‘locale/driver’
Sketchup.require ‘locale/version’

Try:

Sketchup.require 'somtum/gems/locale/locale/tag'
Sketchup.require 'somtum/gems/locale/locale/taglist'
Sketchup.require 'somtum/gems/locale/locale/driver'
Sketchup.require 'somtum/gems/locale/locale/version'

Mayby to try Sketchup.load instead of Sketchup.require? The load method is used to include encrypted and nonencrypted ruby files according to API Docs.

Mayby to try Sketchup.load instead of Sketchup.require ?

This will end up failing as well since require is just an alias of load. My guess is somtum replaced require_relative calls with Sketchup.require. If they were originally require calls, I don’t see how it would have worked unless ‘Z:/shared/plug/src/somtum/gems/locale’ was in $LOAD_PATH (in which case Sketchup.require shouldn’t have failed).

1 Like

Yea, looks like it’s an issue with the relative paths. They need to be relative to the Plugins directory.

Also, when using a gem in your extension, if you publish it, make sure to wrap everything into your own namespace. Otherwise it will be rejected by extension warehouse. Reason being is that if multiple extensions bundled the same gem - but of different versions - you would clash.

1 Like

I’m using this little method to replace require_relative when embedding libraries in my plugins.

# Load file relative to callers location. Unlike Kernel.require_relative this
# supports SketchUp's encrypted extension files.
#
# @param path [String]
#
# @return [Boolean] True when file is loaded. False if already loaded.
def self.require_relative(path)
  base = File.dirname(caller_locations(1, 1).first.absolute_path)
  path = File.join(base, path)

  Sketchup.require(path)
end
1 Like

But where to put this function so it was visible(or overriden) everywhere?

Good question!

From what I know it can’t override the kernel method only within my module. What I do is a simple search and replace throughout the embedded libraries to make them reefer to my own method. It could probably be done with refinements too but that would still require changes to all files using it.

Sure it can. Why do say it cannot ?

Firstly, the definition you show above is a module method and that cannot override an instance method that is inherited.

But if you define it as a local instance method of your module or class, then it WILL override the mixed in method from Kernel. (ie, require_relative is defined in Kernel which is included into Object, which is the superclass of Module, which is the superclass of Class.)

Seeing as how you are likely to do this many times … this is the perfect situation for a mixin library module (just as Kernel is itself a mixed in library module.)

Assume the following saved in a "lib/require_relative_for_sketchup.rb" file …

module AuthorNamespace

  module Lib
    module RequireRelativeForSketchUp
    
      # Load file relative to callers location. Unlike Kernel.require_relative
      # this supports SketchUp's encrypted extension files.
      #
      # @param path [String]
      #
      # @return [Boolean] True when file is loaded. False if already loaded.
      def require_relative(path)
        base = File.dirname(caller_locations(1, 1).first.absolute_path)
        path = File.join(base, path)

        Sketchup.require(path)
      end

    end
  end

end

Then, in another file…

module AuthorNamespace
  
  LIBPATH ||= File.join(__dir__,"lib")
  require File.join(LIBPATH,"require_relative_for_sketchup.rb")

  module SomePlugin

    extend self
    include Lib::RequireRelativeForSketchUp
    
    # other definitions: constants, methods, etc.
    
    class SomeTool
    
      include Lib::RequireRelativeForSketchUp
    
    end  
    
    # more definitions: methods, menu code, etc.
  
  end

end

If you find later that the code for the override method needs adjustment, the fix need only be applied one place in the codebase.

1 Like

@somtum

I think you might investigate adding the real path of somtum/gems/locale to $LOAD_PATH, assuming that Sketchup.require isn’t re-inventing the wheel.

Using a mix-in is ok, but still requires adding code to potentially a lot of files.

Trimble/SketchUp should consider:

  1. Adding Sketchup.require_relative

  2. Same for autoload (Kernel.autoload and Module#autoload). This might make it easier for plug-in devs to not set their code up for 'load everything on startup"…

  3. Consider aliasing the original methods, and replacing them with the Sketchup versions.

That will be rejected in extension reviews. If extensions add paths that belong to their extension to the $LOAD_PATH there can be conflicts between other extensions when they try to resolve require paths.

As things are now - tweak your bundled gems to use Sketchup.require with paths relative to the Plugins directory. (And I hope you have changed the namespace of your bundled gem to be confined to your own extension’s namespace?)

1 Like

Seems odd, but I suppose a malicious actor could…

I suspect the best solution is to compute full paths and use them in requires? If everything is namespaced, a constant equal to __dir__ could be set in the top namespace file…

Hmmm … This is a case where long time Rubyists using the SketchUp API have taken for granted the knowledge of how to load code files. We know that Sketchup.require uses Kernel.require for non-encrypted files, so therefore it’s documentation applies to Sketchup.require as well with regard to the use of absolute paths versus relative paths from the $LOAD_PATH array members.

But, I never realized that the API doc for Sketchup.require / Sketchup.load says nothing of this. It’s docstring should at the least mention that relative paths will be relative to the “Plugins” path for extensions installed in the user’s “AppData” / “Application Support” directories.

Not necessary unless it is in the minority of extensions installed with standalone installers into special paths other than the current version’s %AppData% or %ProgramData% paths (which are added as needed to the Ruby $LOAD_PATH array.)

It is just an educational issue. Many new coders are not first learning the basics of core Ruby before diving into SketchUp extension programming.

At one time (before the API documentation was generated by YARD and served up by GitHub, … it was hosted on SketchUp servers and had some introductory tutorial pages that I think explained the basics of loading code files. We lost these files when we switched to using YARD.

Well, that’s only because SU add the Plugins directory to the $LOAD_PATH.

I think I backed up the old API docs before we switched to YARD. I have to dig in some old backup archive.

The WayBackMachine have most of the old docs, but it appear the Getting Started pages are missing:
https://web.archive.org/web/20120311205755/https://developers.google.com/sketchup/docs/classes

YES, and the docs should mention this so as to inform the coder why relative paths from the “Plugins” path will work, as well as a link to the Kernel.require docs.

Yes. It was under: Introduction > Tutorials > Loading Ruby Scripts