Stage-packages not working for Ruby apps?

Running a commandline ruby app in snapcraft doesn’t seem to work, I’m not sure if I’m doing something wrong or if this is a bug in stage-packages.

The folowing is a minimal test to reproduce the issue.

name: snaplib
version: '0.1'
summary: librarian-puppet
description: |
  librarian-puppet

grade: devel
confinement: devmode

apps:
  librarian-puppet:
    command: librarian-puppet

parts:
  my-part:
    plugin: nil
    stage-packages:
      - librarian-puppet

$ snap install snaplib_0.1_amd64.snap --devmode --dangerous
snaplib 0.1 installed
$ snaplib.librarian-puppet 
/snap/snaplib/x1/command-librarian-puppet.wrapper: 6: exec: librarian-puppet: not found

Looks from https://packages.ubuntu.com/xenial/all/librarian-puppet/filelist like the binary “librarian-puppet” is in /usr/share/librarian-puppet/bin/ by default in the deb, which you’re pulling in. That won’t be in the path in the snap, so maybe change your command line to:

command: usr/share/librarian-puppet/bin/librarian-puppet

This is weird…

$ snaplib.librarian-puppet 
/snap/snaplib/x1/command-librarian-puppet.wrapper: 6: exec: /snap/snaplib/x1/usr/share/librarian-puppet/bin/librarian-puppet: not found

$ /snap/snaplib/x1/usr/share/librarian-puppet/bin/librarian-puppet 
Commands:
  librarian-puppet clean           # Cleans out the cache and install paths.
  librarian-puppet config          # Show or edit the config.
  librarian-puppet help [COMMAND]  # Describe available commands or one specific command
  librarian-puppet init            # Initializes the current directory.
  librarian-puppet install         # Resolves and installs all of the dependencies you specify.
  librarian-puppet outdated        # Lists outdated dependencies.
  librarian-puppet package         # Cache the puppet modules in vendor/puppet/cache.
  librarian-puppet show            # Shows dependencies
  librarian-puppet update          # Updates and installs the dependencies you specify.
  librarian-puppet version         # Displays the version.

Why doesn’t the wrapper find that file?

I wonder if the launcher is doing some magic. The first line of librarian-puppet is #!/usr/bin/ruby which doesn’t exist inside your snap, but you probably have it installed on your host. So when you run the script directly it works (finding the ruby binary) but inside the snap environment /usr/bin/ruby doesn’t exist.

Probably a factor.

yeah, the wrapper (or the “command:” line) should probably call:

$SNAP/usr/bin/ruby $SNAP/usr/share/librarian-puppet/bin/librarian-puppet

I get a buildtime error when I use $SNAP/usr/bin/ruby $SNAP/usr/share/librarian-puppet/bin/librarian-puppet:

Priming my-part 
Traceback (most recent call last):
  File "/usr/bin/snapcraft", line 9, in <module>
    load_entry_point('snapcraft==2.34', 'console_scripts', 'snapcraft')()
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 542, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 2569, in load_entry_point
    return ep.load()
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 2229, in load
    return self.resolve()
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 2235, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/usr/lib/python3/dist-packages/snapcraft/cli/__main__.py", line 19, in <module>
    run(prog_name='snapcraft')
  File "/usr/lib/python3/dist-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python3/dist-packages/click/core.py", line 1060, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python3/dist-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python3/dist-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/snapcraft/cli/lifecycle.py", line 132, in snap
    project_options, directory=directory, output=output)
  File "/usr/lib/python3/dist-packages/snapcraft/internal/lifecycle.py", line 349, in snap
    execute('prime', project_options)
  File "/usr/lib/python3/dist-packages/snapcraft/internal/lifecycle.py", line 130, in execute
    _Executor(config, project_options).run(step, part_names)
  File "/usr/lib/python3/dist-packages/snapcraft/internal/lifecycle.py", line 228, in run
    self._create_meta(step, part_names)
  File "/usr/lib/python3/dist-packages/snapcraft/internal/lifecycle.py", line 269, in _create_meta
    self.config.snapcraft_yaml_path)
  File "/usr/lib/python3/dist-packages/snapcraft/internal/meta.py", line 84, in create_snap_packaging
    packaging.write_snap_yaml()
  File "/usr/lib/python3/dist-packages/snapcraft/internal/meta.py", line 111, in write_snap_yaml
    snap_yaml = self._compose_snap_yaml()
  File "/usr/lib/python3/dist-packages/snapcraft/internal/meta.py", line 276, in _compose_snap_yaml
    snap_yaml['apps'] = self._wrap_apps(self._config_data['apps'])
  File "/usr/lib/python3/dist-packages/snapcraft/internal/meta.py", line 385, in _wrap_apps
    self._wrap_app(app, apps[app])
  File "/usr/lib/python3/dist-packages/snapcraft/internal/meta.py", line 392, in _wrap_app
    app[k] = self._wrap_exe(app[k], '{}-{}'.format(k, name))
  File "/usr/lib/python3/dist-packages/snapcraft/internal/meta.py", line 365, in _wrap_exe
    with open(exepath, 'rb') as exefile:
FileNotFoundError: [Errno 2] No such file or directory: '/root/build_snaplib/prime/$SNAP/usr/bin/ruby'

Using usr/bin/ruby usr/share/librarian-puppet/bin/librarian-puppet I get a bit further; it builds, executes and crashes:

$ snap install snaplib_0.1_amd64.snap  --devmode --dangerous
snaplib 0.1 installed
$ snaplib.librarian-puppet 
<internal:gem_prelude>:4:in `require': cannot load such file -- rubygems.rb (LoadError)
	from <internal:gem_prelude>:4:in `<internal:gem_prelude>'

I’m honestly a bit surprised that it takes this much effort to include a simple apt dependency in a snap, I thought most of the snap magic was invisible to the snapped app itself? I was under the impression that any stage-package would just be overlaid on top of the root filesystem, as if it was installed on the core snap…?

You probably want to override the RUBYLIB environment variable to specify search paths for ruby to find it’s supporting libraries. Without doing that, until the layouts feature lands, ruby will be looking in /usr/... instead of $SNAP/usr/.... The default search paths are:

/usr/local/lib/site_ruby/2.3.0
/usr/local/lib/x86_64-linux-gnu/site_ruby
/usr/local/lib/site_ruby
/usr/lib/ruby/vendor_ruby/2.3.0
/usr/lib/x86_64-linux-gnu/ruby/vendor_ruby/2.3.0
/usr/lib/ruby/vendor_ruby
/usr/lib/ruby/2.3.0
/usr/lib/x86_64-linux-gnu/ruby/2.3.0

so you can specify in your yaml:

apps:
  librarian-puppet:
    command: usr/bin/ruby $SNAP/usr/share/librarian-puppet/bin/librarian-puppet
    environment:
      RUBYLIB: '$SNAP/usr/local/lib/site_ruby/2.3.0:$SNAP/usr/local/lib/x86_64-linux-gnu/site_ruby:$SNAP/usr/local/lib/site_ruby:$SNAP/usr/lib/ruby/vendor_ruby/2.3.0:$SNAP/usr/lib/x86_64-linux-gnu/ruby/vendor_ruby/2.3.0:$SNAP/usr/lib/ruby/vendor_ruby:$SNAP/usr/lib/ruby/2.3.0:$SNAP/usr/lib/x86_64-linux-gnu/ruby/2.3.0'
2 Likes

Welcome to app confinement. The tools try hard to make this all work out of the box, but applications that have been around a while often don’t expect to be confined, or make some assumptions about the world around them. For many applications it is as easy as staging things and you’re done. Go applications for example have some of the simplest yamls, but we’ve snapped a lot of go apps in our time. I think you’re just pushing at the door of Ruby applications in snaps, and finding sharp corners we’ve not seen before.

1 Like

For future reference, you can find my semi-working librarian-puppet snap here: https://github.com/galgalesh/librarian-puppet-snap

Currently, librarian-puppet install fails when the snap is strictly confined because snaps can’t chmod or chown, and snapcraft-preload doesn’t seem to help.