Skip to content

cross-platform application execution

Notifications You must be signed in to change notification settings

kevgo/run-that-app

Repository files navigation

Run That App!

linux windows

Minimalistic cross-platform application runner

Run-that-app executes native CLI applications on Linux, Windows, macOS, and BSD without the need to install them first. Installation across all possible operating systems is a complex and nuanced problem without a good solution. Run-that-app bypasses this problem.

Run-that-app is minimalistic and completely non-invasive. It ships as a single stand-alone binary. Run-that-app uses no magic, no configuration changes, no environment variables, no application shims or stubs, no shell integrations, no dependencies, no plugins, no need to package applications to install in a special way, no application repository, no Docker, no system daemons, no sudo, no emulation, no WASM, no bloat. Applications download in 1-2 seconds, and store very little (just the executables) on your hard drive. Applications execute at 100% native speed.

quickstart

on Linux or macOS

  1. Install the run-that-app executable:

    curl https://raw.githubusercontent.com/kevgo/run-that-app/main/download.sh | sh
  2. Run an app (in this case actionlint at version 1.6.26)

    ./rta actionlint@1.6.26

on Windows (Powershell)

  1. Download the run-that-app executable:

    Invoke-Expression (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/kevgo/run-that-app/main/download.ps1" -UseBasicParsing).Content
  2. Run an app (in this case actionlint at version 1.6.26)

    .\rta actionlint@1.6.26

installing the run-that-app executable into a specific directory

The installer script places the run-that-app executable into the current directory. To install in another directory, change into that directory and then execute the installer from there.

configuration

You can configure the versions of applications that run-that-app should use in a .tool-versions file that looks like this:

actionlint 1.6.26
shellcheck 0.9.0

Now you can run these applications without having to provide their version numbers:

rta actionlint

Executing rta --setup creates a template of this file for you.

usage

rta [run-that-app arguments] <app name>[@<app version override>] [app arguments]

Arguments for run-that-app come before the name of the application to run. The application name is the first CLI argument that doesn't start with a dash. All CLI arguments after the application name are passed to the application.

Run-that-app Arguments:

  • --available: signal via exit code whether an app is available on the local platform
  • --error-on-output: treat all output of the executed application as an error condidion
  • --help or -h: show help screen
  • --log: enable all logging
  • --log=domain: enable logging for the given domain
    • see the available domains by running with all logging enabled
  • --optional: if there is no pre-compiled binary for your platform, do nothing. This is useful for non-essential applications that shouldn't break
  • --update: updates the versions in .tool-versions automation if they are not available.
  • --which: displays the path to the installed executable of the given application
  • --version or -V: displays the version of run-that-app
  • --versions=<number>: displays the given amount of most recent versions of the given app

The app version override should consist of just the version number, i.e. 1.6.26 and not v1.6.26.

examples

Runs ShellCheck version 0.9.0 with the arguments --color=always myscript.sh.

rta shellcheck@0.9.0 --color=always myscript.sh

Use globally installed applications

If your system already has certain apps installed, run-that-app can use them. Consider this .tool-versions file:

go system 1.21.3

If your computer has Go installed, run-that-app would try to run it. Only if that fails would it install and run Go version 1.21.3.

Run-that-app considers restrictions declared by your code base. If your codebase has a file go.mod containing go 1.21 and the externally installed Go version is older, run-that-app would not use the external version.

Ignore unavailable applications

ShellCheck is just a linter. If it isn't available on a particular platform, the tooling shouldn't abort with an error but simply skip ShellCheck.

rta --optional shellcheck@0.9.0 --color=always myscript.sh

Access the installed executables

This example calls go vet with alphavet as a custom vet tool. But only if alphavet is available for the current platform.

rta --available alphavet && go vet "-vettool=$(rta --which alphavet)" ./...

Usage in a Makefile

Here is a template for installing and using run-that-app in a Makefile:

RTA_VERSION = 0.6.0

# an example Make target that uses run-that-app
test: tools/rta@${RTA_VERSION}
	tools/rta actionlint

# this Make target installs run-that-app if it isn't installed or has the wrong version
tools/rta@${RTA_VERSION}:
	@rm -f tools/rta*
	@mkdir -p tools
	@(cd tools && curl https://raw.githubusercontent.com/kevgo/run-that-app/main/download.sh | sh)
	@mv tools/rta tools/rta@${RUN_THAT_APP_VERSION}
	@ln -s rta@${RUN_THAT_APP_VERSION} tools/rta

You would have to .gitignore the files tools/rta*.

npm and npx

Run-that-app executes the npm and npx executables that come with the Node.js installation. Hence, to install them, you need to provide the Node version. To use already installed executables in your PATH, you need to provide the versions of npm and npx.

Example .tool-versions for npm:

npm system@10.2 20.10.0

This tries to use an existing npm installation as long as it has version 10.2 or higher. If your machine has no npm installed, this installs Node 20.10.0 and uses the npm version that comes with it.

gofmt

Gofmt is distributed as part of a Go installation. So please provide the Go version when specifying the desired gofmt version. Example .tools-versions file:

gofmt 1.21.6

This installs Go 1.21.6 and calls the gofmt contained in this installation.

Q&A

Run-that-app does not support an application I need

It's super easy to add a new application to run-that-app! See DEVELOPMENT.md for details.

Why not use the package manager of my system to install third-party applications?

If it works then do it. We have found many challenges with using executables installed by package managers:

  • Other people might use other operating systems that have other package managers. You would have to support Homebrew, Nix, Scoop, Chocolatey, winget, DNF, pacman, apt, pkg, snap, zypper, xbps, portage, etc.
  • Some environments like Windows or bare-bones Docker images might not have a package manager available.
  • Some of the tools you use might not be available via every package manager. An example are tools distributed via GitHub Releases.
  • You might need a different version of an application than the one provided by your or other people's package manager. A best practice for reproducible builds is using tooling at an exactly specified version instead of whatever version your package manager gives you on a particular day.
  • You might work on several projects, each project requiring different versions of tools.

Why not use Docker?

Docker is overkill for running simple applications that don't need a custom Linux environment. Docker isn't available natively on macOS and Windows. Docker often uses Gigabytes of hard drive space. Docker doesn't help with different CPU architectures (Intel, ARM, Risc-V). Using Docker on CI can cause the Docker-in-Docker problem.

Why not quickly write a small Bash script that downloads the executable?

These Bash scripts tend to become complex if you want them to work well on a variety of operating systems. They require additional applications like curl, gzip, and tar, which must exist on all machines that your Bash script runs on. Bash itself as well as these external dependencies come in a variety of versions and flavors that sometimes aren't compatible with each other.

You also need to write a Powershell script since Bash isn't available out-of-the-box on Windows. Even if Bash is installed on Windows, it executes in an emulated environment that behaves different than a real Linux or Unix system.

Run-that-app saves you from these headaches.

What if an app does not distribute binaries for my platform?

Run-that-app can compile applications from source. If that doesn't work, it can skip non-essential applications like linters via the --optional switch.

What if I compile an app myself?

Add the app that you compiled to the PATH and add a "system" version in the configuration file that looks like this:

acme 1.2.3 system

This tries to first install and run the app named acme at version 1.2.3. If this is not successful, run-that-app looks for an application named acme in the PATH and executes it instead. In this case run-that-app does not guarantee that the app has the correct version.

You can restrict the acceptable versions of the globally installed applications like this:

acme 1.2.3 system@1.2.*

This tries to first install and run the app named acme at version 1.2.3. If this is not successful, run-that-app looks for an application named acme in the PATH, determines its version, and if that version matches the given semver restrictions, executes it. For example, if you have acme at version 1.2.1 installed somewhere in your PATH, run-that-app would execute it.

What about apps is written in NodeJS, Python, or Ruby?

Use the package managers of those frameworks to run that app!

What if my app has dependencies that run-that-app doesn't support?

Use Docker or WASI.

Why does run-that-app not have a marketplace that I can submit my application to?

Run-that-app has such a marketplace, it is embedded into its executable. This has several advantages.

  1. It's much better to use a proper programming language rather than some data format like JSON or YML to define applications that run-that-app is aware of. You get really strong type checking (not just basic JSON-Schema linting), intelligent auto-completion, much more flexibility in how you implement downloading and unpacking archives or installing an application in other ways, and the ability to run automated tests.

    Defining a new app in means copy-and-pasting the definition of an existing app and replacing a few strings. This can be done equally easily in JSON or a programming language.

  2. Having a separate marketplace would result in two separate codebases that are versioned independently of each other: the version of run-that-app and the version of the marketplace. Two separate versions lead to fun problems like an older versions of run-that-app not able to work with newer versions of the marketplace. This severely limits how the data format of the marketplace can evolve. An embedded marketplace does not have this problem. Run-that-app can make breaking changes to the marketplace data without that resulting in a breaking change to the solution itself.

  3. If run-that-app would use an external marketplace, it have to check the version of the local replica of that marketplace at each invocation, and determine if it needs to download updates for the marketplace. And then sometimes download updates. This introduces delays that might be acceptable for package managers that get called once to install an app, but not for an app runner that gets called a lot to execute the apps directly.

  4. Even with an external marketplace, you would still need to update the run-that-app executable regularly. So why not just do that and save yourself the hassle to also update a separate marketplace.

Related solutions

These other cross-platform package managers might be a better fit for your use case.

asdf

Asdf is the classic cross-platform application runner. It is a mature and stable platform that installs a large variety of applications. You load asdf plugins that tell asdf how to install applications. It can create global or local shims for installed applications. Downsides of asdf are that it is written in Bash, which makes it slow and non-portable to Windows.

Compared to asdf, run-that-app also supports Windows, offers conditional execution, allows writing application installation logic in a robust programming language that eliminates most runtime errors, and is faster.

mise

Mise is a rewrite of asdf in Rust. It allows installing applications, sets up shims and shell integration.

Compared to rtx, run-that-app also supports Windows, offers conditional execution, and allows writing application installation logic in a robust programming language that eliminates most runtime errors.

pkgx

Pkgx is a more full-fledged alternative to run-that-app with more bells and whistles, a better user experience, better shell integration, and more polished design. It comes with its own app store that apps need to be listed in to be installable. These is (or at least used to be) a blockchain component to this.

Compared to pkgx, run-that-app is leaner, supports more platforms (Windows), and offers additional features like the ability to compile from source, optional execution, and checking whether an application is available for your platform.