Skip to content

Instantly share code, notes, and snippets.

@paddor
Last active May 17, 2024 09:25
Show Gist options
  • Save paddor/b03b3c500a00ed05cbb4a6c247bec900 to your computer and use it in GitHub Desktop.
Save paddor/b03b3c500a00ed05cbb4a6c247bec900 to your computer and use it in GitHub Desktop.
Snapcraft Ruby plugin for core20
This is one way to build a snap for a Ruby app that:
* uses Ruby 3.3 with YJIT
* uses ZMQ/CZMQ, Sqlite3, libxml2, libmbedtls, libmysqlclient
* bundles custom fonts and wktohtmlpdf for PDF generation
* has a daemon (system service) which uses the 'network-bind' plug
* respects the env vars FOOBAR_APP (path to app) and FOOBAR_CONF (path to config file)
* has a separate, bundled web UI (unpacks webui-dist.tgz to /app/webui/dist inside the snap)
* builds the snap using an LXD container
HOW TO USE
==========
Put the ruby.py script into snap/plugins/ directory of your app.
Put the snapcraft.yml into the snap/ directory of your app.
Put the snapify.sh script into the root directory of your script.
The snapify.sh script packages the bundle (gems) into a tarball and doesn't update it unless necessary, which speeds up the snapping process.
Build artefacts are kept out of the final snap file to reduce size.
It works for our use case. Use at your own risk.
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2021 Patrik Wenger <paddor@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""The ruby plugin is useful for ruby based parts.
It uses ruby-install to install Ruby and can use bundler to install
dependencies from a `Gemfile` found in the sources.
Additionally, this plugin uses the following plugin-specific keywords:
- ruby-version:
(string)
The version of ruby you want this snap to run. (e.g. '3.0' or '2.7.2')
- ruby-flavor:
(string)
Other flavors of ruby supported by ruby-install (e.g. 'jruby', ...)
- ruby-gems:
(list)
A list of gems to install.
- ruby-use-bundler
(boolean)
Use bundler to install gems from a Gemfile (defaults 'false').
- ruby-prefix:
(string)
Prefix directory for installation (defaults '/usr').
- ruby-shared:
(boolean)
Build ruby as a shared library (defaults 'false').
- ruby-use-jemalloc:
(boolean)
Build ruby with libjemalloc (defaults 'false').
- ruby-configure-options:
(array of strings)
Additional configure options to use when configuring ruby.
"""
import os
import re
import logging
from typing import Any, Dict, List, Set
from snapcraft.plugins.v2 import PluginV2
class PluginImpl(PluginV2):
@classmethod
def get_schema(cls) -> Dict[str, Any]:
return {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"additionalProperties": False,
"properties": {
"ruby-flavor": {
"type": "string",
"default": "ruby",
},
"ruby-version": {
"type": "string",
"default": "3.0",
"pattern": r"^\d+\.\d+(\.\d+)?$",
},
"ruby-use-bundler": {
"type": "boolean",
"default": False,
},
"ruby-prefix": {
"type": "string",
"default": "/usr",
},
"ruby-use-jemalloc": {
"type": "boolean",
"default": False,
},
"ruby-shared": {
"type": "boolean",
"default": False,
},
"ruby-configure-options": {
"type": "array",
"items": {"type": "string"},
"default": [],
},
"ruby-gems": {
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
}
def get_build_snaps(self) -> Set[str]:
return set()
def get_build_packages(self) -> Set[str]:
packages = {"curl"}
if self.options.ruby_use_jemalloc:
packages.add("libjemalloc-dev")
return packages
def get_build_environment(self) -> Dict[str, str]:
env = {
"PATH": f"${{SNAPCRAFT_PART_INSTALL}}{self.options.ruby_prefix}/bin:${{PATH}}",
}
if self.options.ruby_shared:
# for finding ruby.so when running `gem` or `bundle`
env["LD_LIBRARY_PATH"] = f"${{SNAPCRAFT_PART_INSTALL}}{self.options.ruby_prefix}/lib${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}"
return env
def _configure_opts(self) -> List[str]:
configure_opts = [
"--without-baseruby",
"--enable-load-relative",
"--disable-install-doc",
] + self.options.ruby_configure_options
if self.options.ruby_shared:
configure_opts.append("--enable-shared")
if self.options.ruby_use_jemalloc:
configure_opts.append("--with-jemalloc")
return configure_opts
def get_build_commands(self) -> List[str]:
# NOTE: To update ruby-install version, go to https://github.com/postmodern/ruby-install/tags
ruby_install_version = '0.9.3'
# NOTE: To update SHA256 checksum, run the following command (with updated version) and copy the output (one line) here:
# curl -L https://github.com/postmodern/ruby-install/archive/refs/tags/v0.9.3.tar.gz -o ruby-install.tar.gz && sha256sum --tag ruby-install.tar.gz
ruby_install_checksum = 'SHA256 (ruby-install.tar.gz) = 3e920c9c770ce040cdf71cc64b809e861b613c570c6113ee61ab1d2885a16ab3'
configure_opts = ' '.join(self._configure_opts())
commands = ['uname -a', 'env']
# NOTE: Download and verify ruby-install and use it to download, compile, and install Ruby
commands.append(f"curl -L --proto '=https' --tlsv1.2 https://github.com/postmodern/ruby-install/archive/refs/tags/v{ruby_install_version}.tar.gz -o ruby-install.tar.gz")
commands.append("echo 'Checksum of downloaded file:'")
commands.append("sha256sum --tag ruby-install.tar.gz")
commands.append("echo 'Checksum is correct if it matches:'")
commands.append(f"echo '{ruby_install_checksum}'")
commands.append(f"echo '{ruby_install_checksum}' | sha256sum --check --strict")
commands.append("tar xfz ruby-install.tar.gz")
commands.append(f"ruby-install-{ruby_install_version}/bin/ruby-install --src-dir ${{SNAPCRAFT_PART_SRC}} --install-dir ${{SNAPCRAFT_PART_INSTALL}}{self.options.ruby_prefix} --package-manager apt --jobs=${{SNAPCRAFT_PARALLEL_BUILD_COUNT}} {self.options.ruby_flavor}-{self.options.ruby_version} -- {configure_opts}")
# NOTE: Update bundler and avoid conflicts/prompts about replacing bundler
# executables by removing them first.
commands.append(f"rm -f ${{SNAPCRAFT_PART_INSTALL}}{self.options.ruby_prefix}/bin/{{bundle,bundler}}")
commands.append("gem update --system")
commands.append("gem install --env-shebang --no-document bundler")
if self.options.ruby_use_bundler:
commands.append("bundle")
if self.options.ruby_gems:
commands.append("gem install --env-shebang --no-document {}".format(' '.join(self.options.ruby_gems)))
return commands
name: foobar
base: core20
version: '0.1.0'
summary: Foobar example snapcraft app
description: |
Foobar example snapcraft app with daemon
grade: stable
confinement: strict
parts:
ruby:
plugin: ruby
ruby-version: '3.3'
ruby-shared: true
build-packages:
- zlib1g-dev
- libssl-dev
- rustc
prime:
- -usr/bin/bundler
- -usr/bin/erb
- -usr/bin/racc
- -usr/bin/rake
- -usr/bin/rbs
- -usr/bin/rdoc
- -usr/bin/ri
- -usr/bin/typeprof
- -usr/include
- -usr/lib/ruby/gems/*/doc
- -usr/share
- -usr/share/man
- -usr/usr/share/doc
wkhtmltopdf:
source: https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
source-type: deb
plugin: dump
stage-packages:
- fontconfig
- libjpeg8
- libpng16-16
- libx11-6
- libxrender1
- libssl1.1
- xfonts-75dpi
- xfonts-base
- zlib1g
override-stage: |
snapcraftctl stage
ln -sf ../local/bin/wkhtmltopdf $SNAPCRAFT_STAGE/usr/bin/wkhtmltopdf
stage:
# remove original config file
- -etc/fonts/fonts.conf
prime:
- etc/fonts
# - usr/bin/fc-list
- usr/bin/wkhtmltopdf
- usr/local/bin/wkhtmltopdf
- usr/lib
- usr/share/fonts/truetype
- lib
fontconfig:
after:
- wkhtmltopdf
source: snap/local
plugin: dump
organize:
# use our own config file
fonts.conf: etc/fonts/
app:
source: .
plugin: dump
organize:
bin: app/bin
lib: app/lib
conf: app/conf
conf.rb.template: app/
Gemfile*: app/
stage:
- app
bundle:
after:
- ruby
- app
plugin: dump
source: vendor_cache.tar.gz
build-packages:
- autoconf
- automake
- git
- cmake
- pkg-config
- libczmq-dev
- libsqlite3-dev
- libmysqlclient-dev
- libxml2-dev
- python3
- python3-pip
- libmbedtls-dev
stage-packages:
- libsqlite3-dev
- libmysqlclient-dev
- libczmq-dev
- libxml2-dev
- libmbedtls-dev
override-build: |
env
pip install conan
command -v conan
command -v bundle
bundle version
cd $SNAPCRAFT_STAGE/app
bundle config set --local deployment true
bundle config set --local path $SNAPCRAFT_PART_INSTALL/app/vendor/bundle
bundle config set --local cache_path $SNAPCRAFT_PART_SRC
bundle config set --local silence_root_warning true
bundle config set --local jobs 1
bundle env
if [ -e $SNAPCRAFT_PART_SRC/../cache/bundle ]
then
mkdir -p $SNAPCRAFT_PART_INSTALL/app/vendor/
cp -R $SNAPCRAFT_PART_SRC/../cache/bundle $SNAPCRAFT_PART_INSTALL/app/vendor/
fi
if bundle check
then
echo "Bundle is already installed."
else
echo "Installing bundle ..."
bundle install --local
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.gem' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.a' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.o' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name 'gem_make.out' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.h' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.c' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.md' -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name 'test' -type d -print0 | xargs -0 rm -rf
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name 'spec' -type d -print0 | xargs -0 rm -rf
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name 'configure' -type f -delete
find $SNAPCRAFT_PART_INSTALL/app/vendor/bundle -name '*.log' -type f -delete
mkdir -p $SNAPCRAFT_PART_SRC/../cache
cp -R $SNAPCRAFT_PART_INSTALL/app/vendor/bundle $SNAPCRAFT_PART_SRC/../cache/
fi
prime:
- -usr/include
- -usr/share
- -usr/lib/x86_64-linux-gnu/*.a
webui:
source: webui-dist.tgz
plugin: dump
organize:
'*': app/webui/dist/
apps:
foobar:
command: usr/bin/bundle exec $SNAP/app/bin/main
plugs:
- network-bind
environment:
BUNDLE_GEMFILE: $SNAP/app/Gemfile
FOOBAR_APP: $SNAP/app
FOOBAR_CONF: $SNAP_COMMON/conf.rb
BUNDLE_PATH: $SNAP/app/vendor/bundle
daemon:
command: usr/bin/bundle exec $SNAP/app/bin/main start
daemon: simple
restart-condition: always
restart-delay: 15s
plugs:
- network-bind
environment:
BUNDLE_GEMFILE: $SNAP/app/Gemfile
RUBYOPT: --disable-did_you_mean
FOOBAR_APP: $SNAP/app
FOOBAR_CONF: $SNAP_COMMON/conf.rb
BUNDLE_PATH: $SNAP/app/vendor/bundle
XDG_DATA_HOME: $SNAP
XDG_CONFIG_HOME: $SNAP
FONTCONFIG_FILE: $SNAP/etc/fonts/fonts.conf
# wkhtmltopdf:
# command: usr/local/bin/wkhtmltopdf
# environment:
# XDG_DATA_HOME: $SNAP
# XDG_CONFIG_HOME: $SNAP
# FONTCONFIG_FILE: $SNAP/etc/fonts/fonts.conf
#
# fc-list:
# command: usr/bin/fc-list
# environment:
# XDG_DATA_HOME: $SNAP
# XDG_CONFIG_HOME: $SNAP
# FONTCONFIG_FILE: $SNAP/etc/fonts/fonts.conf
#! /bin/sh -e
usage()
{
cat << HEREDOC
Usage: $0
optional arguments:
-h, --help show this help message and exit
HEREDOC
}
while true; do
case "$1" in
-h | --help )
usage; exit;;
-- )
shift; break ;;
* )
break ;;
esac
done
BUNDLE_CACHE_ALL=1
BUNDLE_CACHE_ALL_PLATFORMS=1
BUNDLE_NO_INSTALL=1
export BUNDLE_CACHE_ALL
export BUNDLE_CACHE_ALL_PLATFORMS
export BUNDLE_NO_INSTALL
bundle check
bundle package
tar --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2022-12-19' -c vendor/cache | gzip -n > vendor_cache_new.tar.gz
if diff -q vendor_cache.tar.gz vendor_cache_new.tar.gz
then
echo "vendor/cache hasn't changed. Leaving vendor_cache.tar.gz as is."
rm -f vendor_cache_new.tar.gz
else
echo "vendor/cache has changed. Updating vendor_cache.tar.gz ..."
mv vendor_cache_new.tar.gz vendor_cache.tar.gz
fi
snapcraft --use-lxd --debug
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment