diff --git a/.github/workflows/tutorial.yml b/.github/workflows/tutorial.yml index 3d2d039..2a2a0d1 100644 --- a/.github/workflows/tutorial.yml +++ b/.github/workflows/tutorial.yml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true jobs: - lesson-1: + lessons-1-2: runs-on: macos-latest steps: @@ -26,22 +26,56 @@ jobs: - name: Install gem run: gem install tebako - - name: Checkout sample + - name: Checkout uses: actions/checkout@v4 - - name: Package + - name: Package 1_hello_world run: | - tebako press --root=tutorial/1_hello_world/sample --entry=hello_world.rb + tebako press --root=tutorial/1_hello_world/hello_world.sample --entry=hello_world.rb - - name: Run packaged application + - name: Run packaged 1_hello_world application run: | ./hello_world otool -L hello_world - - name: Package with a difefrent name + - name: Package 1_hello_world with a difefrent name run: | - tebako press --root=tutorial/1_hello_world/sample --entry=hello_world.rb --output=lesson-1 + tebako press --root=tutorial/1_hello_world/hello_world.sample --entry=hello_world.rb --output=lesson-1 - - name: Run packaged application with a diffeernt name + - name: Run packaged 1_hello_world application with a different name run: | ./lesson-1 + + - name: Package 2_packaging_scenarios gemfile sample + run: | + tebako press -r tutorial/2_packaging_scenarios/gemfile.sample -e ticker.rb -o ticker.tebako. + + # Cannot run this sample since GH Actions does not allow such websocket connection to the outside world + + - name: Package 2_packaging_scenarios gemspec and gemfile sample + run: | + tebako press -r tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample -e tebako-table-cli -o table.tebako + + - name: Run packaged 2_packaging_scenarios gemspec and gemfile sample + run: | + ./table.tebako + + - name: Package 2_packaging_scenarios gemspec sample + run: | + tebako press -r tutorial/2_packaging_scenarios/gemspec.sample -e tebako-table-cli -o table.tebako + + - name: Run packaged 2_packaging_scenarios gemspec sample + run: | + ./table.tebako + + - name: Package 2_packaging_scenarios gem sample + run: | + mkdir -p tutorial/2_packaging_scenarios/gem.sample + pushd tutorial/2_packaging_scenarios/gemspec.sample + gem build tebako-table.gemspec -o ../gem.sample/tebako-test-0.0.2.gem + popd + tebako press -r tutorial/2_packaging_scenarios/gem.sample -e tebako-table-cli -o table.tebako + + - name: Run packaged 2_packaging_scenarios gems sample + run: | + ./table.tebako diff --git a/.gitignore b/.gitignore index 8982b7c..6a9ea43 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ log # Tutorila hello_world source_filesystem/ +*.tebako +gem.sample/ \ No newline at end of file diff --git a/tutorial/1_hello_world/Lesson-1.adoc b/tutorial/1_hello_world/Lesson-1.adoc index f450505..0474b63 100644 --- a/tutorial/1_hello_world/Lesson-1.adoc +++ b/tutorial/1_hello_world/Lesson-1.adoc @@ -59,7 +59,7 @@ Now we can package it with Tebako: [source,sh] ---- -tebako press --root=sample --entry=hello_world.rb +tebako press --root=hello_world.sample --entry=hello_world.rb ---- This command uses two mandatory parameters: `--root` and `--entry`. @@ -103,7 +103,7 @@ We can use `--output` to specify the name of the package: [source,sh] ---- -tebako press --root=sample --entry=hello_world.rb --output=lesson-1 +tebako press --root=hello_world.sample --entry=hello_world.rb --output=lesson-1 ---- This command creates an executable file `lesson-1` that contains the runtime, the Ruby library files, and the application. diff --git a/tutorial/1_hello_world/hello_world.sample/hello_world.rb b/tutorial/1_hello_world/hello_world.sample/hello_world.rb new file mode 100644 index 0000000..240ac48 --- /dev/null +++ b/tutorial/1_hello_world/hello_world.sample/hello_world.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 1 + +puts "Hello, World!" diff --git a/tutorial/1_hello_world/sample/hello_world.rb b/tutorial/1_hello_world/sample/hello_world.rb deleted file mode 100644 index b4b42b5..0000000 --- a/tutorial/1_hello_world/sample/hello_world.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2025 [Ribose Inc](https://www.ribose.com). -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# Tebako tutorial: Lesson 1 - -puts "Hello, World!" diff --git a/tutorial/2_packaging_scenarios/Lesson-2.adoc b/tutorial/2_packaging_scenarios/Lesson-2.adoc new file mode 100644 index 0000000..5c3fba9 --- /dev/null +++ b/tutorial/2_packaging_scenarios/Lesson-2.adoc @@ -0,0 +1,244 @@ += Tebako Tutorial - Lesson 2: "Packaging Scenarios" + +In Lesson 1, we packaged a simple script without dependencies. Now we will discuss more complex scenarios. +In this lesson, we show how to use Tebako for different layouts of solutions to be packaged. + +== Packaging a Bundle + +Let's package a script that has external dependencies. + +[source,Ruby] +---- +require "async" +require "async/http" +require "async/websocket" + +URL = "wss://stream.binance.com:9443/ws/btcusdt@bookTicker" + +Signal.trap("INT") do + puts "\n\nStopping..." + exit(0) +end + +Async do |task| + endpoint = Async::HTTP::Endpoint.parse(URL, alpn_protocols: Async::HTTP::Protocol::HTTP11.names) + + Async::WebSocket::Client.connect(endpoint) do |connection| + while message = connection.read + puts message.parse + end + end +end +---- + +This script receives the BTC/USDT ticker from the Binance exchange and outputs it to the console. +It uses the `async`, `async-http`, and `async-websocket` gems, so we add a Gemfile to manage dependencies: + +[source,Ruby] +---- +source "https://rubygems.org" + +gem "async" +gem "async-http" +gem "async-websocket" +---- + +We put the script into the `gemfile.sample/ticker.rb` file and the Gemfile into the `gemfile.sample/Gemfile` file. We then package it with Tebako. +Short aliases for the parameters are used: + +[source,sh] +---- +tebako press -r gemfile.sample -e ticker.rb -o ticker.tebako +---- + +Note that we do not run `bundle install` before packaging. Tebako creates its own environment, isolated from the system where we package. +It works similarly to rbenv. When packaging starts, the environment is initialized with Tebako-patched Ruby, and `bundle install` is executed by Tebako +against this environment. + +You can see this sequence in the Tebako console log: + +[source] +---- +-- Running init script + ... creating packaging environment at /Users/runner/.tebako/o/s +-- Running deploy script + ... installing tebako-runtime gem + ... @ /Users/runner/.tebako/o/s/bin/gem install tebako-runtime --no-document --install-dir /Users/runner/.tebako/o/s/lib/ruby/gems/3.2.0 + ... deploying Gemfile + ... @ /Users/runner/.tebako/o/s/bin/bundle config set --local build.ffi --disable-system-libffi + ... @ /Users/runner/.tebako/o/s/bin/bundle config set --local build.nokogiri --no-use-system-libraries + ... @ /Users/runner/.tebako/o/s/bin/bundle config set --local force_ruby_platform false + *** It may take a long time for a big project. It takes REALLY long time on Windows *** + ... @ /Users/runner/.tebako/o/s/bin/bundle install --jobs=3 + ... target entry point will be at /__tebako_memfs__/local/ticker.rb + ... stripping the output +---- + +== Packaging a Gem + +The most common entity for packaging is a previously developed gem. Note that Tebako is an executable packager. This means that we can package +a gem as an application but not as a library. Practically, it means that Tebako packages runs of the gem executables, which serve as the package's +entry point. + +We will use the following gem specification (`gemfile.sample/tebako-table.gemspec`): + +[source,Ruby] +---- +require_relative "lib/version" + +Gem::Specification.new do |s| + s.name = "tebako-test" + s.version = Test::VERSION + s.summary = "A simple gem for Tebako testing" + s.authors = ["Ribose"] + s.email = ["open.source@ribose.com"] + s.files = Dir.glob("lib/**/*") + Dir.glob("exe/**/*") + s.homepage = "https://github.com/tamitebako" + s.license = "Unlicense" + s.bindir = "exe" + s.required_ruby_version = Gem::Requirement.new(">= 2.7.0") + s.add_dependency "text-table", "~> 1.2.4" + s.executables << "tebako-table-cli" +end +---- + +Trivial Gemfile: + +[source,Ruby] +---- +source "https://rubygems.org" +gemspec +---- + +And three source files: + +1. `gemspec_and_gemfile.sample/exe/tebako-table-cli`: + +[source,Ruby] +---- +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "tebako-table" + +instance = Test::TebakoTable.new +instance.run +---- + +2. `gemspec_and_gemfile.sample/lib/tebako-table.rb`: + +[source,Ruby] +---- +require "text-table" +require_relative "version" + +module Test + class TebakoTable + def msg + table = Text::Table.new + table.head = %w[A B] + table.rows = [%w[a1 b1]] + table.rows << %w[a2 b2] + table + end + + def run + puts <<~MSG + Running packaged tebako-table gem version #{VERSION}. + You shall see a nice text table below. + + #{msg} + MSG + end + end +end +---- + +3. `gemspec_and_gemfile.sample/lib/version.rb`: + +[source,Ruby] +---- +module Test + VERSION = "0.0.2" +end +---- + +The `press` command does not change: + +[source,sh] +---- +tebako press -r gemspec_and_gemfile.sample -e tebako-table-cli -o table.tebako +---- + +But now Tebako recognizes that it packages a gem and applies a different deployment scenario: + +[source] +---- +-- Running init script + ... creating packaging environment at /Users/runner/.tebako/o/s +-- Running deploy script + ... installing tebako-runtime gem + ... @ /Users/runner/.tebako/o/s/bin/gem install tebako-runtime --no-document --install-dir /Users/runner/.tebako/o/s/lib/ruby/gems/3.2.0 + ... collecting gem from gemspec /Users/runner/work/tebako-samples/tebako-samples/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/tebako-table.gemspec and Gemfile + ... @ /Users/runner/.tebako/o/s/bin/bundle config set --local build.ffi --disable-system-libffi + ... @ /Users/runner/.tebako/o/s/bin/bundle config set --local build.nokogiri --no-use-system-libraries + ... @ /Users/runner/.tebako/o/s/bin/bundle config set --local force_ruby_platform false + *** It may take a long time for a big project. It takes REALLY long time on Windows *** + ... @ /Users/runner/.tebako/o/s/bin/bundle install --jobs=3 + ... @ /Users/runner/.tebako/o/s/bin/bundle exec /Users/runner/.tebako/o/s/bin/gem build + /Users/runner/work/tebako-samples/tebako-samples/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/tebako-table.gemspec + ... installing /Users/runner/.tebako/o/r/tebako-test-0.0.2.gem + ... @ /Users/runner/.tebako/o/s/bin/gem install /Users/runner/.tebako/o/r/tebako-test-0.0.2.gem --no-document + --install-dir /Users/runner/.tebako/o/s/lib/ruby/gems/3.2.0 --bindir /Users/runner/.tebako/o/s/bin + ... target entry point will be at /__tebako_memfs__/bin/tebako-table-cli + ... stripping the output +---- + +Tebako installs the gem into its Ruby environment and assumes the entry point is the wrapper created by the `gem` command in the `binary` directory. +Note that the entry point script must be explicitly specified as one of the gem executables. + +== Packaging a Gem Without Bundling + +Tebako also supports gems defined without a Gemfile (not bundled). We can copy the previous example, specify dependencies in the gemspec, remove the Gemfile, and package it with Tebako: + +[source,sh] +---- +tebako press -r gemspec.sample -e tebako-table-cli -o table.tebako +---- + +[source] +---- +-- Running init script + ... creating packaging environment at /Users/runner/.tebako/o/s +-- Running deploy script + ... installing tebako-runtime gem + ... @ /Users/runner/.tebako/o/s/bin/gem install tebako-runtime --no-document --install-dir /Users/runner/.tebako/o/s/lib/ruby/gems/3.2.0 + ... collecting gem from gemspec /Users/runner/work/tebako-samples/tebako-samples/tutorial/2_packaging_scenarios/gemspec.sample/tebako-table.gemspec + ... @ /Users/runner/.tebako/o/s/bin/gem build /Users/runner/work/tebako-samples/tebako-samples/tutorial/2_packaging_scenarios/gemspec.sample/tebako-table.gemspec + ... installing /Users/runner/.tebako/o/r/tebako-test-0.0.2.gem + ... @ /Users/runner/.tebako/o/s/bin/gem install /Users/runner/.tebako/o/r/tebako-test-0.0.2.gem --no-document + --install-dir /Users/runner/.tebako/o/s/lib/ruby/gems/3.2.0 --bindir /Users/runner/.tebako/o/s/bin + ... target entry point will be at /__tebako_memfs__/bin/tebako-table-cli +---- + +This approach is faster but may fail for gems with native extensions since Tebako lacks sufficient control to configure them correctly. +We primarily support this for backward compatibility. + +== Packaging a Built Gem + +Tebako can package one or several prebuilt `*.gem` files: + +[source,sh] +---- +mkdir -p gem.sample +pushd gemspec.sample +gem build tebako-table.gemspec -o ../gem.sample/tebako-test-0.0.2.gem +popd +tebako press -r gem.sample -e tebako-table-cli -o table.tebako +---- + +The same limitations apply as in the previous option. This scenario may fail for gems with native extensions due to Tebako's limited control during configuration. +It is supported primarily for backward compatibility. + +== Live Example + +You can find the complete code for this lesson in the `tutorial/2_dependencies` directory of the `tebako-samples` repository. +The code runs on GitHub Actions via the `tutorial.yml` workflow. diff --git a/tutorial/2_packaging_scenarios/gemfile.sample/Gemfile b/tutorial/2_packaging_scenarios/gemfile.sample/Gemfile new file mode 100644 index 0000000..1dfab40 --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemfile.sample/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +source "https://rubygems.org" + +gem "async" +gem "async-http" +gem "async-websocket" \ No newline at end of file diff --git a/tutorial/2_packaging_scenarios/gemfile.sample/ticker.rb b/tutorial/2_packaging_scenarios/gemfile.sample/ticker.rb new file mode 100644 index 0000000..f5e22b2 --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemfile.sample/ticker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +require "async" +require "async/http" +require "async/websocket" + +URL = "wss://stream.binance.com:9443/ws/btcusdt@bookTicker" + +Signal.trap("INT") do + puts "\n\nStopping..." + exit(0) +end + +Async do |task| + endpoint = Async::HTTP::Endpoint.parse(URL, alpn_protocols: Async::HTTP::Protocol::HTTP11.names) + + Async::WebSocket::Client.connect(endpoint) do |connection| + while message = connection.read + puts message.parse + end + end +end diff --git a/tutorial/2_packaging_scenarios/gemspec.sample/exe/tebako-table-cli b/tutorial/2_packaging_scenarios/gemspec.sample/exe/tebako-table-cli new file mode 100755 index 0000000..c9e6387 --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec.sample/exe/tebako-table-cli @@ -0,0 +1,10 @@ +#!/home/tebako/bin/ruby +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "tebako-table" + +instance = Test::TebakoTable.new +instance.run diff --git a/tutorial/2_packaging_scenarios/gemspec.sample/lib/tebako-table.rb b/tutorial/2_packaging_scenarios/gemspec.sample/lib/tebako-table.rb new file mode 100644 index 0000000..2989caa --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec.sample/lib/tebako-table.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +require "text-table" +require_relative "version" + +module Test + class TebakoTable + def msg + table = Text::Table.new + table.head = %w[A B] + table.rows = [%w[a1 b1]] + table.rows << %w[a2 b2] + table + end + + def run + puts <<~MSG + Running packaged tebako-table gem version #{VERSION}. + You shall see a nice text table below. + + #{msg} + MSG + end + end +end diff --git a/tutorial/2_packaging_scenarios/gemspec.sample/lib/version.rb b/tutorial/2_packaging_scenarios/gemspec.sample/lib/version.rb new file mode 100644 index 0000000..860dfa5 --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec.sample/lib/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +module Test + VERSION = "0.0.2" +end diff --git a/tutorial/2_packaging_scenarios/gemspec.sample/tebako-table.gemspec b/tutorial/2_packaging_scenarios/gemspec.sample/tebako-table.gemspec new file mode 100644 index 0000000..477a8cf --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec.sample/tebako-table.gemspec @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative "lib/version" + +Gem::Specification.new do |s| + s.name = "tebako-test" + s.version = Test::VERSION + s.summary = "A simple gem for tebako testing" + s.authors = ["Ribose"] + s.email = ["open.source@ribose.com"] + s.files = Dir.glob("lib/**/*") + Dir.glob("exe/**/*") + s.homepage = "https://github.com/tamitebako" + s.license = "Unlicense" + s.bindir = "exe" + s.required_ruby_version = Gem::Requirement.new(">= 2.7.0") + s.add_dependency "text-table", "~> 1.2.4" + s.executables << "tebako-table-cli" +end diff --git a/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/Gemfile b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/Gemfile new file mode 100644 index 0000000..87fce7d --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +source "https://rubygems.org" + +gemspec diff --git a/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/exe/tebako-table-cli b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/exe/tebako-table-cli new file mode 100755 index 0000000..c9e6387 --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/exe/tebako-table-cli @@ -0,0 +1,10 @@ +#!/home/tebako/bin/ruby +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "tebako-table" + +instance = Test::TebakoTable.new +instance.run diff --git a/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/lib/tebako-table.rb b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/lib/tebako-table.rb new file mode 100644 index 0000000..2989caa --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/lib/tebako-table.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +require "text-table" +require_relative "version" + +module Test + class TebakoTable + def msg + table = Text::Table.new + table.head = %w[A B] + table.rows = [%w[a1 b1]] + table.rows << %w[a2 b2] + table + end + + def run + puts <<~MSG + Running packaged tebako-table gem version #{VERSION}. + You shall see a nice text table below. + + #{msg} + MSG + end + end +end diff --git a/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/lib/version.rb b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/lib/version.rb new file mode 100644 index 0000000..860dfa5 --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/lib/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Tebako tutorial: Lesson 2 + +module Test + VERSION = "0.0.2" +end diff --git a/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/tebako-table.gemspec b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/tebako-table.gemspec new file mode 100644 index 0000000..477a8cf --- /dev/null +++ b/tutorial/2_packaging_scenarios/gemspec_and_gemfile.sample/tebako-table.gemspec @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative "lib/version" + +Gem::Specification.new do |s| + s.name = "tebako-test" + s.version = Test::VERSION + s.summary = "A simple gem for tebako testing" + s.authors = ["Ribose"] + s.email = ["open.source@ribose.com"] + s.files = Dir.glob("lib/**/*") + Dir.glob("exe/**/*") + s.homepage = "https://github.com/tamitebako" + s.license = "Unlicense" + s.bindir = "exe" + s.required_ruby_version = Gem::Requirement.new(">= 2.7.0") + s.add_dependency "text-table", "~> 1.2.4" + s.executables << "tebako-table-cli" +end