Equinox is shutting down on September 30th, 2021. Read the full announcement here.

Docs

Getting Started

To get started, you will first need to sign up for an Equinox account.

How it works

Equinox helps you sign, package and distribute self-updating Go programs. Equinox is made up of three parts:

  1. The Equinox release tool, a small CLI tool that wraps go build
  2. The Equinox SDK, a small go package that adds self-updating functionality to your app.
  3. The Equinox service, that hosts your binaries, download pages and update patches

The release tool is a small wrapper around go build that cross-compiles your app and digitally signs each binary before uploading it to Equinox. The service packages your app into archives and native installers for each target platform. All of these packages are hosted for you with stable URLs. Equinox automatically generates user-friendly download pages for each app.

Once you release an app via Equinox, you can enable self-updating to the latest version with a few lines of code that call the Equinox SDK.

Install the release tool

First, download the equinox release tool. After you extract it, you may want to move it into your $PATH. (/usr/local/bin is a good choice)

The release tool is distributed with Equinox itself. Soon you'll have a download page just like this one for your own application!

The release tool will cross-compile your application with go build then sign and upload each executable to Equinox to be packaged and hosted for download. There are a number of required inputs you must specify to create a release:

$ equinox release --help

OPTIONS:
   --app               publish release for this equinox app id
   --channel "stable"  publish release to this channel
   --platforms         platform list to build. e.g. 'linux_amd64 darwin_386'
   --signing-key       path to the ecdsa private key for signing releases
   --token             an equinox credential token
   --version           version string of the new release

The version argument is the version string of your application. And platforms is a space separated list of platforms to build for. We don't have app, token, and signing-key yet but we'll create them in the next sections.

Generate a signing key

All apps distributed via equinox must be signed with a private key. Cryptographically signing your releases with a private key allows untrusted third parties to distribute your updated code while providing end-to-end guarantees that updates come only from you, the developer, and no one else.

The Equinox release tool makes it easy to generate a public/private key pair. Make sure to save your private key securely, you'll need to sign all of your future releases with it. We'll include the public key in your app later to verify updates when we add the update code. Never give your private key to anyone.

$ equinox genkey

Private key file              /Users/inconshreveable/equinox.key
Public key file               /Users/inconshreveable/equinox.pub

Create an app

On your account dashboard, the first thing you'll need to do is create a new app. In the Apps section, enter the name of your app and click New App. This generates the app ID we need to pass to the release tool.

Create a credential token

Next on the dashboard, we need to create a credential token. This credential authenticates and authorizes the release tool to upload builds to your account.

In the Credential Tokens section, click New Credential to create a new credential. Equinox does not store your credential tokens for security reasons. Copy it and store it somewhere securely.

Building your first release

Now that we've generated a signing key and created a credential and an application, we're ready to invoke the release tool. The release tool will cross-compile your application, sign and then upload each executable to Equinox to be packaged and hosted for download.

The release tool builds your application by invoking go build for each target platform. The positional arguments you pass to the tool are passed to go build. More docs are in the release command section.

The platforms option is a space separated list of target platforms in the form of $GOOS_$GOARCH to compile for. If you are using Go 1.4 or earlier, you must bootstrap the cross-compilation yourself. More docs are in the cross compilation section.

This is an example invocation of the equinox release tool to build the application github.com/example/tool. You'll need to substitute in your own values for app, token, signing-key and obviously the name of your application.

$ equinox release \
 --version="1.0.0" \
 --platforms="darwin_amd64 linux_amd64" \
 --signing-key=/Users/inconshreveable/equinox.key \
 --app="app_ja6WuaZgwsF" \
 --token="4kj5ypgZj3QeGvGz7LckDGvcAcdmUozUNU8YhVhEg97r7dcmFgy" \
 github.com/example/tool

Congratulations! Your first release is live and available to download.

Adding the update code

Now that you've published your first release to equinox, we're ready to add the code that will enable your app to update itself to the latest version. Create a new function equinoxUpdate() in your application:

package main

import (
  "fmt"

  "github.com/equinox-io/equinox"
)

// assigned when creating a new application in the dashboard
const appID = "app_ja6WuaZgwsF"

// public portion of signing key generated by `equinox genkey`
var publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE62fHuaSP1asZmQ8ikYysNB2VxKd8cV5i
G6WZ7PPD9DKAGoebHzm1D/uT2VEWxtgoZvEkbyhGgBbU5z/aDwIa7YNAIZrrRakV
PTvTEyFY2QbNu96tBlVM78N7Rq2HI8nN
-----END ECDSA PUBLIC KEY-----
`)

func equinoxUpdate() error {
  var opts equinox.Options
  if err := opts.SetPublicKeyPEM(publicKey); err != nil {
    return err
  }

  // check for the update
  resp, err := equinox.Check(appID, opts)
  switch {
  case err == equinox.NotAvailableErr:
    fmt.Println("No update available, already at the latest version!")
    return nil
  case err != nil:
    fmt.Println("Update failed:", err)
    return err
  }
 
  // fetch the update and apply it
  err = resp.Apply()
  if err != nil {
    return err
  }

  fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
  return nil
}

You'll need to substitute in your own value for the appID constant. You must also substitute the contents of the public key file generated by the earlier equinox genkey command for the value of publicKey.

Lastly, you'll need to modify your program code to call the equinoxUpdate() function somewhere! How to do this is app-specific, but most folks like to create an update command, so that a user can run the update process explicitly.

If you just want to test out Equinox with an example program, create a simple main() function:

package main

import (
  "fmt"
  "os"
)

func main() {
  if len(os.Args) == 2 && os.Args[1] == "update" {
    equinoxUpdate()
  }
  fmt.Println("Hello example!")
}

Don't forget to create a new release with the updating code by running the release process again. Notice that we increment the version string to 1.0.1.

$ equinox release \
--version="1.0.1" \
--platforms="darwin_amd64 linux_amd64" \
--signing-key=/Users/inconshreveable/equinox.key \
--app="app_ja6WuaZgwsF" \
--token="4kj5ypgZj3QeGvGz7LckDGvcAcdmUozUNU8YhVhEg97r7dcmFgy" \
github.com/example/tool

Verifying it works

First, let's verify that our downloads page works. Go back to your dashboard and click on your app:

On your app page, you should see that the latest version you released was 1.0.1. It will also show a link to the public download page for your app. Click on it to check out your download page:

Download the released version of your app and then run the update functionality. For the example program we created above, it would look like this:

inconshreveable [~] ♡ : ./example update
No update available, already at the latest version!

Great! But we want to see some updating happen. Let's modify our program just a little so we can make sure that the update works as we expect:

-   fmt.Println("Hello example!")
+   fmt.Println("Hello updated example!")

It's time to build and release the application again. Run equinox release with a new version string. Following the example, it will be --version="1.0.2" this time.

Finally, now that a new version is released, we can run the whole update process!

$ ./example
Hello example!

$ ./example update
Updated to new version: 1.0.2!

$ ./example
Hello updated example!

Domain Model

Apps

An app is a just a Go program which you want to distribute and manage updates for. Apps are independent and you can manage multiple apps with a single Equinox account.

Releases

A release is the set of packages for all platforms of a specific version of your app. Releases correspond one-to-one with versions of your app (not always - there may be versions that you choose not to release). In fact, the version string you specify when creating a release must be unique for that app.

When a release is published it is immutable. You may not add, remove, or change the uploaded binaries. You may, however, revoke a release, which will make it unavailable for any further downloads or updates.

Channels

When you upload a release to Equinox, you don't associate it with an app. Instead your app has release channels. When you create a release, you choose on which channel that release should be published. This allows you to create a beta channel where you can push new releases of experimental code and a stable channel for more production-ready releases.

Releases can be published to more than one channel. This can enable workflows where a release is promoted from one channel to another. For example, some projects use a workflow where, after enough time and testing, releases on a beta channel are then published to a stable channel.

Releasing

The release tool

The release tool wraps go build. It compiles your application for each target platform and digitally signs each binary before uploading to Equinox.

You can download the latest version of the release tool from here. After you extract it, you may want to move it into your $PATH (/usr/local/bin is a good choice). If you already have the tool installed, make sure you have the most recent version by running equinox update.

Build process

For each platform, the release tool takes the following steps:

  • Invoke go build with the supplied command-line options
  • Generate a hex-encoded SHA256 checksum from the binary
  • Digitally sign the checksum using an ECDSA key
  • Upload that binary to Equinox

If your machine has multiple cores, platforms are built in parallel.

Let's see how this works in practice. Let's say you normally build your application with the following invocation of the go tool.

$ go build github.com/acme/rocket

To build and release this program with Equinox, replace go build with equinox release. The Equinox-specific options are covered in the publishing section below.

$ equinox release [options] github.com/acme/rocket

The go build command accepts a wide range of additional build options, such as build tags. If your build process takes advantage of these features, you'll need to include a double-dash after the Equinox-specific options.

$ equinox release [options] -- -tags release github.com/acme/rocket

Cross compilation

Go has fantastic support for cross compilation. Equinox takes advantage of this support, making it easy to build for every target platform in one go.

If you're using Go 1.5+, cross compilation just works and you can skip the remainder of this section.

If you're using Go 1.4 or earlier, you need to build support for each platform you intend to target. First, change into your $GOROOT/src directory.

$ go env
...
GOROOT="/usr/local/go"
...
$ cd /usr/local/go/src

Here you'll find the make.bash script to build each of the needed toolchains.

$ GOOS=windows GOARCH=amd64 ./make.bash --no-clean
$ GOOS=windows GOARCH=386 ./make.bash --no-clean
$ GOOS=linux GOARCH=amd64 ./make.bash --no-clean
$ GOOS=linux GOARCH=386 ./make.bash --no-clean

Code signing

All apps distributed via Equinox must be signed with a private key. Cryptographically signing your releases with a private key allows untrusted third parties to distribute your updated code while providing end-to-end guarantees that updates come only from you, the developer, and no one else.

The Equinox release tool makes it easy to generate a public/private key pair. Make sure to save your private key securely, you'll need to sign all of your future releases with it. You'll include the public key in your app to verify updates when we add the update code.

$ equinox genkey
 
Private key file              /Users/inconshreveable/equinox.key
Public key file               /Users/inconshreveable/equinox.pub

Never give your private key to anyone.

Equinox generates ECDSA public and private keys using the P384 curve.

Specify the path to your private key with the --signing-key option. Alternatively you may set the environment variable EQUINOX_SIGNING_KEY with a PEM-encoded ECDSA private key. This can be helpful when integrating equinox into a CI environment.

Publish a release

Before you can publish your first release, you'll need to create an application and generate a credential token. With both of those and your private signing key, you can run the release command. This is an example release invocation:

$ equinox release \
  --version="1.0.0" \
  --platforms="darwin_amd64 linux_amd64" \
  --signing-key=/Users/inconshreveable/equinox.key \
  --app="app_ja6WuaZgwsF" \
  --token="4kj5ypgZj3QeGvGz7LckDGvcAcdmUozUNU8YhVhEg97r7dcmFgy" \
  github.com/acme/rocket

Equinox treats release versions (--version) as opaque strings. From Equinox's perspective, the newest version of your application is the most recently published release, it has nothing to do with version strings. The Updating section explains this in more detail.

platforms is a comma (or space) separated list of build targets, defined as $GOOS_$GOARCH. A full list of supported operating systems and architectures can be found on the Golang website.

When the release command finishes, your release isn't immediately available. Equinox still needs to package up your binaries for release. The packagin process takes a few minutes at most.

Resuming a release

Sometimes things go wrong (network blip, power outage, alien invasion) and the release process can fail. Equinox gracefully recovers from these interruptions: the release tool is designed to resume where it last left off.

If you encounter a failure, run the release command again with the same arguments. The tool will look for an existing, unpublished release. If one is found, the tool won't upload any artifacts that successfully finished during the previous run.

Promoting a release

The release tool can also publish a release that is already uploaded and published to another channel that it hasn't been published to. You'll get a helpful error message if you try to publish a release to channel that doesn't exist or one that it's already been published to.

By default, each application gets one channel named "stable". You can add additional channels to your application on your dashboard.

A common workflow involves having an additional beta channel. New versions are first pushed to the beta channel for a small number of users to test out. Once your testers confirm the release works as intended, they are published to the stable channel. Here is an example of publishing the existing 0.1 release to the stable channel:

$ equinox publish --token xxx --app app_xxx --channel stable --release 0.1

Configuration file

The release tool supports storing parameters in a configuration file so you can avoid passing them in via the command line. The configuration file has the following strucutre:

# saved in config.yaml
app: app_xxx
signing-key: ./secret.key
token: TOKEN
platforms: [
  darwin_amd64, darwin_386,
  linux_amd64, linux_386,
  windows_amd64, windows_386
]

With the above file saved to config.yaml, you only need to pass three arguments to the release command. You also no longer need to enter your credential token on the command line.

$ equinox release --config ./config.yaml --channel stable --version 0.1 github.com/acme/rocket

The publish command also accepts the --config option.

$ equinox publish --config ./config.yaml --channel beta --release 0.1

Updating

Unlike web applications, getting new code to users of client or on-prem applications is a real challenge. Equinox strives to make this process seamless by allowing you to build experiences similar to the updating functionality in major web browsers that can help keep your users up to date and using the latest version of your software.

How it works

Equinox updates work by including a small SDK into your Go application. The SDK provides simple APIs to perform a check for new updates by consulting the Equinox service. If a new update is available, you can instruct the SDK to download and apply the update. The new code will run on the next invocation of the program.

The Equinox SDK is hosted on github. The SDK takes care of all the aspects of applying a safe cross-platform update including checksum and signature validation.

The SDK exposes two major APIs: Check and Apply. When your app calls Check, it will connect to the Equinox service and ask for the most recently published release on a specified release channel. If there is a more recent version, the call will return information about the new version and a method to Apply the new update. This allows you to build experiences where you prompt the user for permission first before updating.

If you choose to apply the update, the SDK will fetch either the new version of the program or a suitable binary patch, download it, apply it, verify it and then replace the current executable contents with the newly updated code.

What is the latest version?

How does Equinox determine whether a client program is at the latest version? The following rules explain how the update check process works to find the latest version of a client program on a release channel.

  1. When the program checks for an update, the SDK checksums the current binary and sends that value to the update service for the check.
  2. The update service consults the most recently published release on the channel. It determines whether the release has an appropriate binary with a matching OS and architecture. If no matching binary is found, it continues walking back up to previous releases published on the channel.
  3. Once a release with a matching binary is found, the Equinox service compares the supplied checksum with the checksum on record. If they match, the program is up to date. If they do no match, the program is ready for an update.
The version string of both the updating program and the version string of the releases stored on Equinox are never consulted during the update process. Version strings are completely opaque from Equinox's perspective.
Because development builds of your program are never published to Equinox, they will always be ready to update. If you invoke your updating functionality automatically, it's best to disable it in debug builds.

By default the Check API will look for the most recent version. You can also configure it to look for a specific version instead of the lastest.

Update failures

Updates can fail for many reasons. The most common cause of update failures is insufficient file system permissions to apply the update. For an update to be applied succesfully, the running code must have the following permissions:

  1. Read permission on the executable file itself
  2. Write permission on the directory the executable file is in

These requirements are most often not satisfied when the binary is copied to a privileged system directory like /usr/bin but invoked by the user without root privileges. The equinox SDK checks this for you and will return a helpful error message if that's the case.

Of course, updates may still fail because of network problems, system resource exhaustion and other sources. If an error is returned by applying the update, that means the entire process failed and the old code is still there and running. Your program will never 'partially update'.

There is one pathological case where the update may fail in a way that the old binary is renamed but the new binary is not installed. This almost never happens in practice, and if it does, Equinox will return a helpful error message for the user. See this documentation for more details.

Binary diffs

All possible updates will be computed as binary patches by Equinox. You don't have to enable anything! It's all handled automatically and greatly improves update speed. These binary patches often reduce the download size to just a few hundred kilobytes instead of the 10+ MB of a new Go binary.

Binary patches can only be computed if the updating program was previously published in an Equinox release. If it was not, Equinox will handle the update by returning a full copy of the latest program.

Adding the equinox update SDK

Please consult the relevant section of the getting started section and the API documentation for the Equinox SDK.

Automated Packaging

After the release tool uploads the binaries for a release, Equinox packages them before it publishes the release. These packages are archives or native installers that make it easy for users to start working with your software. Equinox currently packages your binaries into zip and tar.gz archives for each platform.

The filename of your binary when it is packaged into an archive or installer is the app slug that you can configure on your Equinox dashboard.

zip / tgz

Equinox packages your binaries into zip and tgz archives for all platforms. These archives are configured so that the binary will extract into the current working directory and will have the proper executable permission mode bits (0755) set.

.pkg for OS X

Equinox can automatically build .pkg installers for all OS X (darwin) artifacts in a release. The packages install your application into /usr/local/bin/ on the target system.

A known bug is that OS X .pkg installers require root privileges to install. The installed binary will be owned by root, it can still be successfully updated without root privileges.

OS X .pkg installers are only available on the Business plan.

OS X .pkg installers are not signed. When a .pkg is not signed, OS X's Gatekeeper will refuse to install the application without user opt-in and display a strong warning message discouraging installation.

Homebrew tap for OS X

Equinox can automatically publish OS X releases to a custom Homebrew tap. Once a user "taps" your repository, they can install the application using the brew command.

$ brew tap eqnxio/{account-slug}
$ brew install {app}

Users can also install the application directly without tapping the repository first.

$ brew install eqnxio/{account-slug}/{app}

Homebrew support is only available on the business plan.

.msi for Windows

Equinox can automatically build .msi installers for all Windows binary artifacts in a release. These are per-user installers (not machine-wide) so they do not require Administrator privileges.

Your application will be installed into the user's local programs directory (e.g. C:\Users\name\AppData\Local\Programs\yourapp). The user's PATH environment variable will be updated to find your application. An uninstaller shortcut will be placed in the start menu.

Windows .msi installers are only available on the Business plan.

Windows .msi installers are not signed. When a .msi installer is not signed, Windows may present a strong warning or fail to install without explicit user opt-in.

.deb for Linux

Equinox can automatically build .deb installers for all Linux binary artifacts in a release. The .deb packages install your application into /usr/local/bin/ on the target system. The installed files will be owned by the installing user (usually root), which means that only the root user will be able to initiate an update.

Linux .deb installers are only available on the Business plan.

.rpm for Linux

Equinox can automatically build .rpm installers for all Linux binary artifacts in a release. The .rpm packages install your application into /usr/local/bin/ on the target system. The installed files will be owned by the installing user (usually root), which means that only the root user will be able to initiate an update.

Linux .rpm installers are only available on the Business plan.