Shell command (executable) fails when launched with backticks from SketchUp

I have created a script that uses the gsl gem. Because this is a native gems and it cannot be installed in SketchUp I created a self contained executable in Windows 10 with ocra. I am using Ruby 2.7.2 installed with rubyinstaller and all the various packages installed with msys and pacman

The executable works perfectly when launched from the Windows command line (even in a completely fresh installation), but when launched from the Ruby console in Sketchup or from a script, it fails.

If you want to test it, this is the script (no code, just the require statements)

require "tmpdir"
require "json"
require "gsl"

and this is the actual executable (zipped) generated by ocra and this is the actual folder it creates

I have tried with backticks, Open3, exec, system and I always get the same results

rubygems/core_ext/kernel_require.rb:92:in `require': cannot load such file -- gsl (LoadError)

TBH, I am not sure it is a SketchUp related issue, but because it does work from a normal console it makes me think that it could be related to how SU handles the execution of shell commands.

Any suggestion or idea? This is above my pay grade :slight_smile:

I am moving all the calculations to the cloud, but this is a bit urgent because of a new customer’s request and this approach ‘seemed’ the quickest.

groundtest.exe.zip (6.5 MB)
ocr2BE.tmp.zip (10.9 MB)

This is a LoadError. This means that SketchUp’s embedded Ruby process cannot find the file named “gsl” with any of the expected file types for the platform (as mentioned in the doc for Kerenl#require.)

SketchUp’s embedded Ruby differs from a standard Ruby system process in that it does not use several of the environment variables that a standalone Ruby process does.

The README for the gsl gem mentions that the standard Ruby environment variable LD_LIBRARY_PATH would need to be set to where the GSL libraries are installed. SketchUp would not be using this path by default. Your code would likely need to put these libraries in your extension’s folder or perhaps push the required path(s) onto the $LOAD_PATH array so SketchUp’s Ruby can find the files for the gem and the GNU libraries.


By the way, SketchUp seizes the standard IO for use with it’s own console, and every few versions the output from backtick (%x) string commands gets broken and nothing gets returned. I think it got broken again around the SU2019 release and has not yet been fixed.

For example %x[dir] or `dir` should return the output from the listing of the current directory to SketchUp’s console, but does not. Only an empty string is returned.

There is a workaround and that is to redirect the output of the command to a temp file in some directory. IE, this example uses the current directory ( Dir.pwd ) whatever it may be.

`dir > dir.txt`
puts File.read("dir.txt")

If you wanted to store the result into a string for later parsing, then …

text = File.read("dir.txt")

… or …

lines = File.readlines("dir.txt")

You should read other topics concerning gems in SketchUp. SketchUp is a shared environment and it is not really acceptable for gems and libs to be creating classes in the top level ObjectSpace. The next extension might be trying to load a newer version of the same gem or lib that is incompatible with the one yours loaded.

In these other threads it has been mentioned several times that gems and libs need to be wrapped inside your own namespace module(s) so that they will not conflict with any other extensions.

Perhaps this is why you tried to run the gsl gem in an external process ? (It can be daunting to modify the C code for gems and libs to wrap within your extension’s namespaces.)

1 Like

A few other alternatives for running external apps on Windows

SketchUp Ruby API’s UI::openURL can start executables.

Example: from the Ruby Console in SketchUp 2022 I call this …

UI.openURL("file:///C:/Program Files/SketchUp/SketchUp 2017/SketchUp.exe")

… and after a few seconds an instance of SU2017 is loaded. (I have like 6 versions of SketchUp installed.)

Another example:

UI.openURL("file:///C:/Windows/notepad.exe")

… opens an instance of Window’s Notepad editor.


The Windows Scripting Host Shell Object can be used from within SketchUp using Ruby’s WIN32OLE library.

require 'win32ole'
shell = WIN32OLE.new("WScript.Shell")

shell.Exec("calc")

… or …

shell.Run "cmd /K CD C:\ & Dir"
1 Like

That is the reason. If I had more time I would rewrite the script in C and use Fiddle.

The idea was to write an independent script, convert it into a self contained executable and then execute the file as a shell command. These are the steps:

  1. The main Ruby scripts writes an ‘input.json’ in Dir.tmpdir
  2. launches the shell executable with backticks or similar
  3. the shell executable reads the input file, does the calculations and saves the results in output.json in Dir.tmpdir
  4. finally the main Ruby scripts reads the output file from the temp dir.

What I do not understand is why the executable can find gsl when launched outside SketchUp and not when launched from inside Sketchup. If you see the content of the ocra folder I attached, it has its own ruby.exe.

See my post above whilst you were typing.

You did not attach it. It is in cloud dropbox. I no longer have that installed for various reasons.

Attached now in the original post.

Thanks for looking into this.

What is/was the current directory when running OCRA on the sources ?

What is the shell command you use to run the OCRA exe file ?

Did you change the working directory to the OCRA exe location before running the shell command ?

Where does (or should) the OCRA extractor write the app folder ?

Did you use the --chdir-first OCRA option when building the self extracting executable ?

C:\Users\ruggiero\dev\rubyexe

This folder contains the script ground.rb. From here I launch the following ocra command:

ocra ground.rb --dll ruby_builtin_dlls\libssp-0.dll --dll libgsl-27.dll --dll libgslcblas-0.dll --console --verbose --gem-full --add-all-core --gem-full=gsl --chdir-first

libgsl-27.dll and libgslcblas-0.dll are in C:\Ruby27-x64\bin while libssp-0.dll is in C:\Ruby27-x64\bin\ruby_builtin_dlls

The command above creates the ground.exe file that I execute from the same folder just typing ground.exe

No need because I was already in that folder

Ocra writes the app folder if you add the debug flag --debug-extract and it saves it in the same folder. Without the debug flag it creates a temporary folder in C:/Users/user/AppData/Local/Temp and it deletes after execution.

Yes. See command above

These questions are meant from the POV of within SketchUp.


Try changing the top of your “ground.rb” script to:

info = "$LOAD_PATH contents:\n\n"
info << $:.join("\n")
info << "\n\n"
info << "ENV contents:\n\n"
ENV.each do |key,val|
  info << "\"#{key}\" : \"#{val}\"\n"
end

require "tmpdir"

IO.write(File.join(Dir.tmpdir,'ground_info.txt'), info)

require "json"
require "gsl"

We would especially be interested in the file written when run from within SketchUp.

From the Ruby console I run

`"C:/Users/ruggiero/dev/rubyexe/ground.exe" 2> err.log`

and the content of the error log is

C:/Users/ruggiero/AppData/Local/Temp/ocrB9BD.tmp/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require': cannot load such file -- gsl (LoadError)
	from C:/Users/ruggiero/AppData/Local/Temp/ocrB9BD.tmp/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require'
	from C:/Users/ruggiero/AppData/Local/Temp/ocrB9BD.tmp/src/ground.rb:17:in `<main>'

If I add your code and run it with the same command above from within SketchUp, this is the content of ground_info.txt

$LOAD_PATH contents:

C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/site_ruby/2.7.0
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/site_ruby/2.7.0/x64-msvcrt
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/site_ruby
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/vendor_ruby/2.7.0
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/vendor_ruby/2.7.0/x64-msvcrt
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/vendor_ruby
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/2.7.0
C:/Users/ruggiero/AppData/Local/Temp/ocrAA55.tmp/lib/ruby/2.7.0/x64-mingw32

ENV contents:

"ALLUSERSPROFILE" : "C:\ProgramData"
"APPDATA" : "C:\Users\ruggiero\AppData\Roaming"
"ChocolateyInstall" : "C:\ProgramData\chocolatey"
"ChocolateyLastPathUpdate" : "133019151631305445"
"CommonProgramFiles" : "C:\Program Files\Common Files"
"CommonProgramFiles(x86)" : "C:\Program Files (x86)\Common Files"
"CommonProgramW6432" : "C:\Program Files\Common Files"
"COMPUTERNAME" : "RUGGIEROGUI0C5E"
"ComSpec" : "C:\WINDOWS\system32\cmd.exe"
"CONDA_BAT" : "C:\Users\ruggiero\miniconda3\condabin\conda.bat"
"CONDA_EXE" : "C:\Users\ruggiero\miniconda3\Scripts\conda.exe"
"CONDA_SHLVL" : "0"
"DriverData" : "C:\Windows\System32\Drivers\DriverData"
"FPS_BROWSER_APP_PROFILE_STRING" : "Internet Explorer"
"FPS_BROWSER_USER_PROFILE_STRING" : "Default"
"GEM_HOME" : "C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Gems64"
"GEM_PATH" : "C:\Users\ruggiero\AppData\Local\Temp\ocrAA55.tmp\gemhome"
"HOME" : "C:/Users/ruggiero"
"HOMEDRIVE" : "C:"
"HOMEPATH" : "\Users\ruggiero"
"LOCALAPPDATA" : "C:\Users\ruggiero\AppData\Local"
"LOGONSERVER" : "\\RUGGIEROGUI0C5E"
"NUMBER_OF_PROCESSORS" : "4"
"OCRA_EXECUTABLE" : "C:\Users\ruggiero\dev\rubyexe\ground.exe"
"OneDrive" : "C:\Users\ruggiero\OneDrive"
"OPENMODELICAHOME" : "C:\Program Files\OpenModelica1.18.0-64bit\"
"OS" : "Windows_NT"
"Path" : "C:\Users\ruggiero\miniconda3\condabin;C:\Program Files\SketchUp\SketchUp 2021\Tools\RubyStdLib\platform_specific;C:\Program Files\SketchUp\SketchUp 2017\;C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\Radiance\bin;C:\Program Files\Git\cmd;C:\ProgramData\chocolatey\bin;C:\Strawberry\c\bin;C:\Strawberry\perl\site\bin;C:\Strawberry\perl\bin;C:\Users\ruggiero\miniconda3\condabin;C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\Radiance\bin;C:\Program Files\Git\cmd;C:\ProgramData\chocolatey\bin;C:\Strawberry\c\bin;C:\Strawberry\perl\site\bin;C:\Strawberry\perl\bin;C:\Ruby27-x64\bin;C:\Users\ruggiero\AppData\Local\Microsoft\WindowsApps;C:\Users\ruggiero\AppData\Local\Programs\Microsoft VS Code\bin;C:\tools;C:\Apps\sqlite;C:\Program Files\7-Zip;C:\Users\ruggiero\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\Scripts;C:\Users\ruggiero\AppData\Local\bin\NASM;"
"PATHEXT" : ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.RB;.RBW"
"PROCESSOR_ARCHITECTURE" : "AMD64"
"PROCESSOR_IDENTIFIER" : "Intel64 Family 6 Model 70 Stepping 1, GenuineIntel"
"PROCESSOR_LEVEL" : "6"
"PROCESSOR_REVISION" : "4601"
"ProgramData" : "C:\ProgramData"
"ProgramFiles" : "C:\Program Files"
"ProgramFiles(x86)" : "C:\Program Files (x86)"
"ProgramW6432" : "C:\Program Files"
"PROMPT" : "$P$G"
"PSModulePath" : "C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules"
"PUBLIC" : "C:\Users\Public"
"RAYPATH" : "C:\Program Files\Radiance\lib\;."
"RLM_DIAGNOSTICS" : "C:\ProgramData\SketchUp\SketchUp 2021\liclog.txt"
"RUBYLIB" : ""
"RUBYOPT" : "-Eutf-8"
"SESSIONNAME" : "Console"
"SSL_CERT_FILE" : "C:/Program Files/SketchUp/SketchUp 2021/Tools/cacert.pem"
"SU_CEF_WEBHELPER_PARENT_PID" : "2432"
"SystemDrive" : "C:"
"SystemRoot" : "C:\WINDOWS"
"TEMP" : "C:\Users\ruggiero\AppData\Local\Temp"
"TMP" : "C:\Users\ruggiero\AppData\Local\Temp"
"USER" : "ruggiero"
"USERDOMAIN" : "RUGGIEROGUI0C5E"
"USERDOMAIN_ROAMINGPROFILE" : "RUGGIEROGUI0C5E"
"USERNAME" : "ruggiero"
"USERPROFILE" : "C:\Users\ruggiero"
"windir" : "C:\WINDOWS"

Okay, … when you run or spawn a process from another, the new process gets a copy of the prior’s environment.

Your script’s temp Ruby environment is picking up the following var from SketchUp’s environment:

"GEM_HOME" : "C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Gems64"

Notice how the "GEM_PATH" variable has a path into the temporary folder:

"GEM_PATH" : "C:\Users\ruggiero\AppData\Local\Temp\ocrAA55.tmp\gemhome"

I’m not sure that the latter is correct as the sample zip you posted above has no “gemhome” subfolder (only "bin", "lib", and "src" folders.)

Definitely, the top of your "ground.rb" file needs to get rid of what it gets from SketchUp.

You could try …

ENV["GEM_HOME"]= ENV["GEM_PATH"]

But I’d suggest seeing what the info is when running outside SketchUp.

I would think that the GEM_HOME path (where gems are installed) would be something like:

"C:\Users\ruggiero\AppData\Local\Temp\ocrAA55.tmp\lib\ruby\gems\2.7.0"

And the GEM_PATH search list always begins with the GEM_HOME path and then has appended any extra search paths, separated by the platform’s path separator character (the File::PATH_SEPARATOR constant.)


So (I think) you can correct the ENV variables (at the top of your script before the “requires”) thus:

temp = File.dirname(__dir__) # parent of the script's directory
ENV["GEM_HOME"]= File.join(temp, "lib/ruby/gems/2.7.0")
ENV["GEM_PATH"]= ENV["GEM_HOME"]

Before digging deeper, I tested this directly and it did not work.

I then added a sleep(30) in the script and copied the temp folder created and I can confirm that it did not have a gemhome folder in it. Not sure when this is created.

I also launched the script outside SU and it did not have a GEM_HOME path, only GEM_PATH and it was

"GEM_PATH" : "C:\Users\ruggiero\AppData\Local\Temp\ocrB266.tmp\gemhome"

But now the bad news…

Just to be sure that the Windows installer was working, I built the solution anyway, even with ground.exe as it is. It turns out that WIndows Real Time Protection identifies this file as a Trojan!! I then discovered that it is a known issue.

The plugin will be installed in a corporate environment and I don’t think there is any chance that I could ask to disable the antivrus for the installation. This approach is starting to look more and more like a dead end.

Thank you very much for the time you have spent looking into it. Really appreciate it.

1 Like

The alternative is to wrap it all up in an installer and install it as an application, rather than running it from the TEMP folder each time.

This is described in the OCRA README, ie: Creating an Installer for your Application.

Ok, I assume you’re familiar with why, namespacing issues, MSVC runtime issues, etc.

But, you can include all the pre-built gem files in your code’s directories, load the gsl dlls using Fiddle and LoadLibraryW, and you should be good to go.

Thanks @MSP_Greg,

Yes I am. That is what I do, more or less, for the other gems on macos and windows, but the gsl gem was not working on WIndows. That is why I got the idea that generated this post, but unfortunately it created an unexpected can of worms :slight_smile:

What was the issue?

That is why I got the idea that generated this post

It’s been a while since I checked, but SU has always had issues with running ‘shell’ commands in it.

JFYI, I forked rb-gsl and ran CI in GitHub Actions. The Ubuntu & macOS jobs all passed, but every Windows job had several failures/errors. So, parts of gsl may work on Windows, but other parts may not.

I may have time to look at it more later, as some of the problems may be the test code, not the lib code…

Embedding gsl files in your plugin should work.

I copied the gsl rb files/folders, gsl_native.so and the two required dlls to a ‘lib’ directory that would be in your plugin.

The main code would load gsl via:

require_relative 'lib/gsl'
puts GSL::VERSION  # added just to see if it loaded

I added the following to the top of gsl.rb. The code loads the two gsl dlls in the correct order, so when gsl_native.so is loaded, it finds its dll dependencies:

$LOAD_PATH << __dir__

if RUBY_PLATFORM =~ /mswin|mingw/
  require 'fiddle'
  kernel32 = Fiddle.dlopen 'kernel32'
  load_library = Fiddle::Function.new(
    kernel32['LoadLibraryW'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT,
  )

  dll_path = "#{__dir__}/libgslcblas-0.dll"
  if load_library.call(dll_path.encode('utf-16le')).zero?
    abort "Failed to load libgslcblas-0.dll from #{dll_path}"
  end

  dll_path = "#{__dir__}/libgsl-27.dll"
  if load_library.call(dll_path.encode('utf-16le')).zero?
    abort "Failed to load libgsl-27.dll from #{dll_path}"
  end
end

It did output 2.7.1, which is the correct version. Note that SU Ruby 2.7 is an mswin ucrt build, and the RubyInstaller2 Ruby 2.7 is a mingw msvcrt build. That runtime mismatch may cause issues.

I think the next release of SU may be built with Ruby 3.1. RubyInstaller2 switched to ucrt builds with Ruby 3.1, so the runtime mismatch issue probably won’t exist…

OK. I tried your code, but I receive the same error. My code is organised in a different way as I copy the full gem in my plugin folder. So I modified the main gsl.rb with

require 'fiddle'

puts "Loading supporting libs"
kernel32 = Fiddle.dlopen 'kernel32'
load_library = Fiddle::Function.new(
kernel32['LoadLibraryW'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT,
)

dll_path = "Y:/MCTools/src/mct-tools/ext/winlibs/2021/libgslcblas-0.dll"
if load_library.call(dll_path.encode('utf-16le')).zero?
   abort "Failed to load libgslcblas-0.dll from #{dll_path}"
else
   puts "libgslcblas loaded"
end

dll_path = "Y:/MCTools/src/mct-tools/ext/winlibs/2021/libgsl-27.dll"
if load_library.call(dll_path.encode('utf-16le')).zero?
   abort "Failed to load libgsl-27.dll from #{dll_path}"
else
   puts "libgsl loaded"
end

require 'gsl_native'

If I use require "gsl" I get the error from my loader.rb

Error Loading File //Mac/dev/MCTools/src/mct-tools/loader.rb
Error: #<LoadError: 127: The specified procedure could not be found.   - //Mac/dev/MCTools/src/mct-tools/ext/win/gems/ruby/2.7.0/gems/gsl-2.1.0.3/lib/gsl_native.so>
C:/Program Files/SketchUp/SketchUp 2021/Tools/RubyStdLib/rubygems/core_ext/kernel_require.rb:92:in `require'
C:/Program Files/SketchUp/SketchUp 2021/Tools/RubyStdLib/rubygems/core_ext/kernel_require.rb:92:in `require'
//Mac/dev/MCTools/src/mct-tools/ext/win/gems/ruby/2.7.0/gems/gsl-2.1.0.3/lib/gsl.rb:32:in `<top (required)>'
C:/Program Files/SketchUp/SketchUp 2021/Tools/RubyStdLib/rubygems/core_ext/kernel_require.rb:92:in `require'
C:/Program Files/SketchUp/SketchUp 2021/Tools/RubyStdLib/rubygems/core_ext/kernel_require.rb:92:in `require'
//Mac/dev/MCTools/src/mct-tools/loader.rb:91:in `<top (required)>'
C:/Program Files/SketchUp/SketchUp 2021/Tools/extensions.rb:197:in `require'
C:/Program Files/SketchUp/SketchUp 2021/Tools/extensions.rb:197:in `load'
//Mac/dev/MCTools/src/mct-tools.rb:44:in `register_extension'
//Mac/dev/MCTools/src/mct-tools.rb:44:in `<module:RG>'
//Mac/dev/MCTools/src/mct-tools.rb:20:in `<top (required)>'
C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Plugins/!external.rb:19:in `require'
C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Plugins/!external.rb:19:in `block (2 levels) in <top (required)>'
C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Plugins/!external.rb:18:in `glob'
C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Plugins/!external.rb:18:in `block in <top (required)>'
C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Plugins/!external.rb:16:in `each'
C:/Users/ruggiero/AppData/Roaming/SketchUp/SketchUp 2021/SketchUp/Plugins/!external.rb:16:in `<top (required)>'

(The path //Mac and Y: are there because I develop on Mac and run Sketchup from a Parallels Desktop virtual machine)

Just to be sure that it finds the file:

File.exists?("//Mac/dev/MCTools/src/mct-tools/ext/win/gems/ruby/2.7.0/gems/gsl-2.1.0.3/lib/gsl_native.so")
true

and the console output (twice for some reason)

Loading supporting libs
libgslcblas loaded
libgsl loaded
Loading supporting libs
libgslcblas loaded
libgsl loaded

And just as I finished writing this message I realised that you are referring to rb-gsl while I am using gsl. Didn’t realise there were two gems. Do you think that could be the cause? If so, as soon as I have a bit more time, I could try the other gem.

Thanks for looking into this!

They renamed it. rb-gsl stopped at 1.16.0.6 - July 03, 2015, gsl is now on 2.1.0.3 - April 29, 2017. Same GutHub repository, named rb-gsl.

require will use RubyGems to resolve a location.

I copy the full gem in my plugin folder.

That’s why the first line of the code above adds $LOAD_PATH << __dir__. You may have a different folder structure, adjust for that. But, RubyGems should not be locating the gem, which is what the location below is:

//Mac/dev/MCTools/src/mct-tools/ext/win/gems/ruby/2.7.0/gems/gsl-2.1.0.3/lib/gsl_native.so

Or, on Windows, gsl_native.so should be in the same folder as gsl.rb, and the code I listed also has the two MSYS2 mingw dlls in that folder.