Fledge packages software written in Ruby so that people can run it without installing Ruby. Point it at the program you already run: a main.rb, a bin/server, or a gem's exe/my_app. Fledge copies the program, its project, and the gems your Gemfile.lock locks into a copy of Ruby that runs from any folder, and writes an app for the platform you build on.
Your users download one file, unpack it wherever they like, and run it. They install nothing, and they never need to know what Ruby is. If your app has windows, Fledge also builds an installer for each system.
Fledge writes these for you, for each platform you build on:
| App kind | macOS | Windows | Linux |
|---|---|---|---|
console (the default) |
.tar.gz holding the program |
.zip holding NAME.exe |
.tar.gz holding the program |
windowed |
NAME.app and a .dmg |
NAME.exe, which opens no console, and a -setup.exe that installs without an administrator |
a folder with a desktop entry and icons, an AppImage, and a .deb if you ask for one |
A program that prints one line builds to a 20 MB archive (62 MB unpacked) on arm64-darwin. Your users run it with no Ruby on their PATH, and a Ruby they installed makes no difference: the launcher hides RUBYOPT, RUBYLIB, GEM_HOME, and the other variables a Ruby reads from the app's boot.
- A gem. Your users install Ruby first, at a version your gems accept.
- A Docker image. Your users install Docker, and your program runs inside Linux.
- Single-file packagers, such as Tebako and Ocran. They unpack the program to a temporary folder, or mount a virtual file system, every time it starts. A Fledge app is a folder that runs where it sits, with Ruby's usual layout inside.
Fledge follows designs that other languages have settled: LÖVE's fused games (a folder joined to a runtime), PyInstaller and Briefcase (console and windowed apps), python-build-standalone (prebuilt interpreters that move), and Tauri's bundler (installers).
| Platform | Your users run it on |
|---|---|
arm64-darwin |
macOS 14 or newer, on Apple silicon |
x86_64-linux, aarch64-linux |
Linux with glibc 2.39 or newer (Ubuntu 24.04, Debian 13) |
x86_64-linux-musl, aarch64-linux-musl |
Linux with musl, such as Alpine |
x64-mingw-ucrt |
Windows 10 and 11 on x64 |
aarch64-mingw-ucrt |
Windows 11 on ARM64 |
On every push, Fledge's CI builds each platform's apps on a computer of that platform, copies them to a clean one that has no Ruby, and runs them there (ci.yml, deliver.sh, deliver.ps1). After each release, installed.yml installs Fledge from the new tag the way you would, and builds and runs the examples.
The Ruby badge at the top shows the version of Ruby inside your app.
Before you start:
- Build on a computer of each platform you ship to: a Mac for macOS, Windows for Windows, Linux for Linux. A CI runner of each works.
- There is no runtime for Intel Macs or 32-bit systems.
- Run Fledge with Bundler, on Ruby 4.0.1 or newer.
An app runs your code as ruby main.rb does, with the same requires, globals, ENV, and standard library. Fledge's CI runs a built app and ruby main.rb side by side on every platform and fails on any difference that tasks/parity.rb does not list with a reason. The listed differences that your code can see:
- Your program's path.
$0,__FILE__, and$PROGRAM_NAMEare the absolute path of the program inside the app, whereruby main.rbgives the path as typed. Your users can start the app from any folder, so a relative path would not hold. Ruby's other answers stay true:__dir__,RbConfig.ruby, andGem.dirname where your program, its Ruby, and its gems are. - Your users' Ruby settings.
RUBYOPT,RUBYLIB,GEM_HOME, and the like do not change how the app boots, so a Ruby your users installed cannot break it. They still appear inENVand reach every program your app starts.$VERBOSEis the visible effect:RUBYOPT=-W0sets it tonilunderruby main.rband not in the app. - Your gems. The app's gems are the runtime's, so
Gem.dirandGem.pathname the runtime's gem folder and never your users'GEM_HOMEor~/.gem. Thedevelopmentandtestgroups are left out..bundle/configmarks the bundle frozen, so Bundler never writesGemfile.lock, even in a read-only folder, and Fledge's RubyGems defaults file loads at start. A bundled gem your lockfile does not lock, such asirb, still activates when Ruby asks for it, sobinding.irbworks in an app with a Gemfile as it does underbundle exec. - The locale. On macOS and Linux, when no locale variable is set, or
LANGisCorPOSIX, as a double-click launch gives, the launcher and the Ruby thatRbConfig.rubynames use UTF-8, whererubywould stay in C.Encoding.default_external,Encoding.find("locale"), andEncoding.find("filesystem")are then UTF-8, not US-ASCII. Without it, a user whose home folder has a non-ASCII name would see Ruby and Bundler fail on the path. AnLC_ALLorLC_CTYPEyour user sets,Cincluded, is used asrubyuses it, and nothing is added toENV. To see it, printEncoding.default_externalfrom your program and run the app withenv -iand again withenv -i LC_ALL=C. If this ever surprises you, open an issue. - Restarts. On macOS and Linux,
exec RbConfig.ruby, $0, *ARGVrestarts the app as it first started, with the same setup. On Windows, start the app'sNAME.exeagain. - Headers. The runtime carries Ruby's C headers, so
require "mkmf"andfind_executablework. It leaves out the static and import libraries, so an app cannot compile an extension. On a phone,mkmfcannot load. - What a standard Ruby installs and the runtime does not. On macOS and Linux, the runtime has no
bootsnapormsgpack, which the Ruby builds it starts from add. On Windows, RubyInstaller's own files do not load at start.
Add Fledge to your Gemfile, in the development group so that fledge build leaves it out of your app:
gem "fledge", git: "https://github.com/setdef/Fledge", tag: "v0.3.0", group: :developmentYou install Fledge from a git tag. The releases page lists the latest.
Run fledge build in your project's folder. Here, the project is a folder named greeter whose main.rb prints Hello, Ruby!:
$ bundle install
$ bundle exec fledge build
Downloading fledge-runtime-0.3.0-arm64-darwin.tar.gz from https://github.com/setdef/Fledge/releases/download/v0.3.0
dist/Greeter-arm64-darwin.tar.gz
$ tar -xzf dist/Greeter-arm64-darwin.tar.gz -C /tmp
$ /tmp/Greeter-arm64-darwin/Greeter
Hello, Ruby!The first time you build, Fledge downloads the runtime for your platform from the release, and checks it against a SHA-256 that it carries. The app takes its name from the folder, camelized. Send the .tar.gz (a .zip on Windows) to anyone on that platform.
With no argument, fledge build builds the folder you are in: the one executable its gemspec names, or else its main.rb. Pass a path to build another program, and --out DIR to write somewhere other than dist. Your whole project ships in the app's app/ folder, except .git, .bundle, vendor/bundle, vendor/cache, and the folders earlier builds wrote to. bundle exec fledge --help prints the whole manual.
Put a fledge.yml beside your Gemfile:
name: My Cool App
kind: windowed
identifier: com.example.MyCoolApp # reverse-DNS; macOS and Windows know the app by it
version: "1.0.0"
icon: icons/app # icons/app.icns, icons/app.ico, icons/app-16.png to -512.png
installers: [dmg, nsis, appimage, deb]macOS, Windows, and Linux show your app's name, icon, and version in their usual places. examples/windowed_app is a complete example, and --help lists every key.
installers chooses which of dmg, nsis, appimage, and deb to build. The default is the first three, and deb needs dpkg-deb on your computer. A .deb depends only on libc6, so apt installs it on a computer without a desktop too. A library a gem opens while your app runs, such as the X11, Wayland, or OpenGL libraries SDL opens, comes from the desktop, and Fledge cannot check that it is there.
If your app has more than one way in, such as a window, a terminal interface, and a command line that all open the same save file, list them under programs in fledge.yml. Fledge builds them into one folder that shares one Ruby runtime, one copy of your project, and one set of gems. fledge build with no argument builds all of them; fledge build PATH with one of the listed files builds that program alone.
name: Shared Save
programs:
- path: window.rb
name: Shared Save
kind: windowed
identifier: com.example.SharedSave
- path: bin/shared-save
kind: consoleAt most one program is windowed. It gets the installers, and on macOS the console programs live inside its .app bundle. examples/programs is a complete example.
An app is unsigned unless you sign it. Without a signature, macOS's Gatekeeper and Windows' SmartScreen warn the people who download your app.
To sign with your own identity, pass --sign. On macOS, give a code signing identity from your keychain, such as "Developer ID Application: NAME (TEAM)". On Windows, give the SHA-1 thumbprint of a code signing certificate in your certificate store. Fledge signs every program and library, the app bundle, and the installer.
Fledge's CI signs with a self-signed certificate. It has not yet tried a certificate from Apple or a certificate authority, so how Gatekeeper and SmartScreen treat one is unconfirmed. Fledge does not notarize; xcrun notarytool does that to a signed disk image.
Your Gemfile.lock decides which gems ship. The development and test groups stay out, and an optional group ships only if your Bundler with setting names it. Lock each platform you build on, and every gem's checksum:
$ bundle lock --add-platform arm64-darwin x86_64-linux x64-mingw-ucrt
$ bundle lock --add-checksumsPrecompiled gems ship as they are. Gems that ship as source, and gems from git, compile on your computer against the runtime's Ruby. Install a compiler first: Xcode's command line tools on macOS, a C compiler and make on Linux, or RubyInstaller's MSYS2 tools (ridk install) on Windows, and cargo for a Rust extension. If a gem would not load on another computer, the build fails and the error names the gem. Git gems, git submodules, path gems, mirrors, and credentials work as they do in Bundler; examples/real_gems uses each.
A gem from a GitHub release, which is not on rubygems.org, has no checksum on any server. bundle lock --add-checksums fills in the gem Bundler installed on your computer and leaves an empty entry for each other platform's build, and fledge build refuses a lockfile with an empty entry. Print each .gem file's SHA-256 and write it after that build's entry in the CHECKSUMS section of Gemfile.lock:
$ shasum -a 256 vendor/cache/my_gem-*.gem
a40dd10e0d71ed27d8ad95aa0db9bb17f7076adaa8fdf45fefd712266759f102 vendor/cache/my_gem-1.0.0-x86_64-linux-gnu.gemCHECKSUMS
my_gem (1.0.0-x86_64-linux-gnu) sha256=a40dd10e0d71ed27d8ad95aa0db9bb17f7076adaa8fdf45fefd712266759f102
If the gem's publisher lists a SHA-256 for each file, compare it with yours. Bundler keeps the checksums you write, and Bundler and Fledge both refuse a .gem that does not match.
For a phone, Fledge does not build the finished app. It writes an Xcode or Gradle project that holds your program and a Ruby runtime, and you build, sign, and submit that project the way you would any other phone app.
fledge build ioswrites an Xcode project for iOS 15 and later. Run it on a Mac with Xcode and XcodeGen.fledge build androidwrites a Gradle project for Android 8.0 (API 26) and later. Run it on a Mac, x86-64 Linux, or Windows. Building the project takes a JDK and the Android SDK.
Lock ruby as a platform (bundle lock --add-platform ruby). examples/mobile_app is a complete example.
Gems with extensions. A gem with a C or C++ extension compiles for the phone and links into the app: with Xcode on iOS, and with the Android NDK on Android (set ANDROID_NDK_ROOT, or install the NDK in the SDK's ndk/ folder). Load the extension with require "name/name", not require_relative. An extconf.rb that runs CMake gets a toolchain file for each target in CMAKE_TOOLCHAIN_FILE, so the library it builds is built for the phone. Fledge refuses, and names the gem, a Rust extension and an extension that the gemspec says Rake, CMake, or configure builds without an extconf.rb. examples/native_phone and examples/cmake_phone show each case.
Frameworks that start the app themselves. A framework such as SDL3 needs its own startup files on each phone. Name a folder of them under ios: host: and android: host: in fledge.yml, and fledge build copies them over the project it writes. examples/sdl_phone is a game loop on SDL3.
Fledge's CI runs phone apps in the iOS Simulator on an iPhone and an iPad, and on x86-64 Android emulators. It has not run an app on a physical iPhone or iPad, and no app has been through App Review. For arm64-v8a, which most Android phones use, the CI builds the library and checks the APK but never runs it, because no free runner boots an arm64 emulator.
Change the tag in your Gemfile and run bundle update fledge. Each version pins the SHA-256 of every runtime it downloads. The Ruby badge at the top shows the Ruby inside your app, and the CHANGELOG records each change to it. Fledge is 0.x: a minor version can break your build, so read the CHANGELOG before you update.
| Resource | Description |
|---|---|
bundle exec fledge --help |
The manual: every option, every fledge.yml key, every file Fledge writes, and the exit statuses |
examples/ |
Apps that CI builds and runs. Start with hello, my_cool_app (a gem), windowed_app, programs (several programs in one app), and native_app (gems compiled from source) |
| What ships in your app | Every component Fledge puts in an app, on each platform, and what its license asks of you |
| CHANGELOG | What changed in each release |
| Releases | Each release's runtimes, with their notices and software bills of materials |
Open an issue. Say which platform you built on, which platform you ran on, and what bundle exec fledge --version prints.
See CONTRIBUTING.md.
Fledge is copyright 2026, Kerrick Long, under the AGPL-3.0-or-later. Documentation is CC-BY-SA-4.0. Code snippets and examples are MIT-0: copy and use them without attribution. See each file's SPDX comment for specifics.
Your app stays yours. You can keep your app's source code private. Two files in each app are Fledge's: the launcher your users run, and one RubyGems defaults file. They are under the AGPL, so when you give the app to people, you give them the source of those two files too. Each app's notices/NOTICES.txt says where it is: Fledge's source, at the tag that built the app. The rest of the runtime is under licenses that ask only that you keep their notices, and every app carries them. Your gems have licenses of their own, and you read them; Fledge ships every gem you lock, whatever its license. What ships in your app lists every component, and is not legal advice. Setdef may also offer Fledge under a commercial license, for people who prefer not to follow the AGPL for those two files.
Some parts of this program are copied from other sources under appropriate reuse licenses, and the copyright belongs to their respective owners. See the REUSE Specification for details.
This program is requested, and its maintainer describes the three ways code comes to exist like this:
I use three words for how code comes to exist. Code I wrote, I typed by hand, with nothing generated. Code I wrought, an AI agent generated while I iterated with it, and I reviewed every change before committing it, so I take responsibility for every diff. Code I requested, an agent built with little or no review from me, in the style of a "dark factory."
All of Fledge is requested. The CI gates and the Delivery jobs, which run each built app on a clean computer, stand in for review. Each commit has a Provenance: trailer and a footer that names the model. On declare-ai.org's scale the assistance is creative, not none, non-creative, or total, because the agents made design and wording choices under the maintainer's direction.