How to release an Ebiten game for Steam

Hajime Hoshi
2021-08-29

I have succeeded to release an Ebiten game "INNO VATION 2007!" at Steam store some days ago. It's a free game. It supports all the platforms Windows, macOS, and Linux. The source code is available at GitHub.

Copyright 2007 Omega

You have to pass a review to release your game at Steam. To pass a review, simply building an Ebiten game by Go is not enough. You have to do additional tasks. This artcile explains necessary items to pass a review for Ebiten games, but doesn't explain general things about Steamworks.

In the below explanation, we assume the game name is yourgame, the user name is Your Name, and so on. Please replace them as appropriate.

Steamworks SDK

There are Steam features like getting the user's language or unlocking achievements. They are available via Steamworks SDK. The formats of the SDK are dynamic libraries like DLL and shared objects, so you have to take ingenuity to use them from Go.

Then, I created a biding called go-steamworks. You can use it just by importing. For example, you can write the process to reopen the application if the application was not opened via Steam client like this.

package main

import (
	"os"

	"github.com/hajimehoshi/go-steamworks"
)

const appID = 480 // Use your application ID.

func init() {
	if steamworks.RestartAppIfNecessary(appID) {
		os.Exit(1)
	}
	if !steamworks.Init() {
		panic("steamworks.Init failed")
	}
}

This binding doesn't implement most of the APIs. I plan to implement them in the future.

Windows

Windows is the easiest, and what you have to do is to build your game by Go in the regular way. On Windows, Ebiten is in pure Go, then you can build it anywhere by specifying GOOS and GOARCH.

On PowerShell, the commands are like this.

$Env:GOARCH = '386'
go build -o yourgame_windows_386.exe .
$Env:GOARCH = 'amd64'
go build -o yourgame_windows_amd64.exe .
Remove-Item Env:GOARCH

On a POSIX shell, the commands are like this.

env GOOS=windows GOARCH=386 go build -o yourgame_windows_386.exe .
env GOOS=windows GOARCH=amd64 go build -o yourgame_windows_amd64.exe .

When buliding a GUI application for Windows, you can specify -ldflags=-H=windowsgui to vanish the console. As Ebiten closes it automatically anyway, it is also fine not to specify this.

An icon is not necessary. If you care, please use a tool to embed resources as appropriate.

Then, compress the exe files as zips, and upload them as builds at Steamworks.

macOS

In the case of macOS, you have to create an application as .app. Besides, you have to get your application notarized by Apple. Apparently, notarizing an application is not mandatory to release games at Steam, but your application would not work at relatively new (10.15?) macOS, then we can say notarizing is a must. You have to register Apple Developer to get your application notarized.

I refered a blog article "Releasing Steam Games on Mac Is a Monster Pain" to write this article.

The architecture is assumed to be amd64. Unfortunately, Steamworks SDK doesn't support M1 (arm64) yet.

First, prepare an icon file as icns format. This format can be exported by opening e.g., an PNG file with Preview.app. If you cannot find icns in the list of exporting formats, it should be shown by opening the list with pressing Alt key.

Then, create a minimal .app like this.

name=yourgame
app_name=YourGame.app
bundle_id=com.example.yourgame

rm -rf ${app_name}
mkdir -p ${app_name}/Contents/MacOS
mkdir -p ${app_name}/Contents/Resources
env CGO_ENABLED=1 CGO_CFLAGS=-mmacosx-version-min=10.12 CGO_LDFLAGS=-mmacosx-version-min=10.12 GOARCH=amd64 go build -o ${app_name}/Contents/MacOS/${name} .
cp icon.icns ${app_name}/Contents/Resources/icon.icns
echo '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleExecutable</key>
    <string>{{.Name}}</string>
    <key>CFBundleIdentifier</key>
    <string>{{.BundleID}}</string>
    <key>CFBundleIconFile</key>
    <string>icon.icns</string>
    <key>CFBundleVersion</key>
    <string>0.0.0</string>
    <key>CFBundleShortVersionString</key>
    <string>0.0.0</string>
    <key>NSHighResolutionCapable</key>
    <true />
    <key>LSMinimumSystemVersion</key>
    <string>10.12.0</string>
  </dict>
</plist>' |
    sed -e "s/{{.Name}}/${name}/g" |
    sed -e "s/{{.BundleID}}/${bundle_id}/g" > ${app_name}/Contents/Info.plist

This specifies -mmacosx-version-min=10.12 at CGO_CLAGS and CGO_LDFLAGS. Without this, your application would not work on an older macOS than yours.

Next, create an app ID (bundle ID) at Apple Developer's account page if you don't have one.

Next, create a certificate of Developer ID (Developer ID Application) at Apple Developer's account page if you don't have one.

Next, create an app-specific password. You can create one easily at Apple ID website. For more details, see the help page.

Next, get your application notarized.

name=yourgame
app_name=YourGame.app
bundle_id=com.example.yourgame
email=yourname@example.com
developer_name='Developer ID Application: Your Name (1234567890)'
asc_provider=1234567890

mkdir -p .cache

echo '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
  </dict>
</plist>' > .cache/entitlements.plist

codesign --display \
         --verbose \
         --verify \
         --sign "${developer_name}" \
         --timestamp \
         --options runtime \
         --force \
         --entitlements .cache/entitlements.plist \
         --deep \
         ${app_name}

ditto -c -k --keepParent ${app_name} ${app_name}.zip

if [[ -z "${APP_SPECIFIC_PASSWORD}" ]]; then
    echo 'fail: set APP_SPECIFIC_PASSWORD. See https://support.apple.com/en-us/HT204397'
    exit 1
fi

xcrun altool --notarize-app \
             --primary-bundle-id "${bundle_id}" \
             --username "${email}" \
             --password "${APP_SPECIFIC_PASSWORD}" \
             --asc-provider "${asc_provider}" \
             --file ${app_name}.zip
rm ${app_name}.zip

After executing these commands, a UUID for the notarization transaction is shown. After 5 minutes or so, you will receive an email from Apple.

If you succeed notarization, execute this command.

xcrun stapler staple YourGame.app

If you want to see notarization logs, execute this command. Replace the arguments with appropriate values. This command shows a URL, which showns logs. If you fail the notarization, the reasons should be written there.

xcrun altool --notarization-info UUID --username YOUR_MAIL_ADDRESS --password APP_SPECIFIC_PASSWORD

When you want to upload your .app as a build at Steamworks, you should not compress this by zip command or a menu at Finder. A notarized .app includes some special files and they might be missing if you create a zip in a regular way. Instead, use ditto command to create a zip.

ditto -c -k --keepParent YourGame.app yourgame_darwin_amd64.zip

Linux

In the case of Linux, Steam Runtime is prepared as a Dockerfile. Buliding your application in this environment is the easiest way.

name=yourgame
STEAM_RUNTIME_VERSION=0.20210817.0
GO_VERSION=$(go env GOVERSION)

mkdir -p .cache/${STEAM_RUNTIME_VERSION}

# Download binaries for 386.
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.Dockerfile ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.Dockerfile)
fi
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.tar.gz ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.tar.gz)
fi
if [[ ! -f .cache/${GO_VERSION}.linux-386.tar.gz ]]; then
    (cd .cache; curl --location --remote-name https://golang.org/dl/${GO_VERSION}.linux-386.tar.gz)
fi

# Download binaries for amd64.
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.Dockerfile ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.Dockerfile)
fi
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.tar.gz ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.tar.gz)
fi
if [[ ! -f .cache/${GO_VERSION}.linux-amd64.tar.gz ]]; then
    (cd .cache; curl --location --remote-name https://golang.org/dl/${GO_VERSION}.linux-amd64.tar.gz)
fi

# Build for 386.
(cd .cache/${STEAM_RUNTIME_VERSION}; docker build -f com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.Dockerfile -t steamrt_scout_i386:latest .)
docker run --rm --workdir=/work --volume $(pwd):/work steamrt_scout_i386:latest /bin/sh -c "
export PATH=\$PATH:/usr/local/go/bin
export CGO_CFLAGS=-std=gnu99

rm -rf /usr/local/go && tar -C /usr/local -xzf .cache/${GO_VERSION}.linux-386.tar.gz

go build -o ${name}_linux_386 .
"

# Build for amd64.
(cd .cache/${STEAM_RUNTIME_VERSION}; docker build -f com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.Dockerfile -t steamrt_scout_amd64:latest .)
docker run --rm --workdir=/work --volume $(pwd):/work steamrt_scout_amd64:latest /bin/sh -c "
export PATH=\$PATH:/usr/local/go/bin
export CGO_CFLAGS=-std=gnu99

rm -rf /usr/local/go && tar -C /usr/local -xzf .cache/${GO_VERSION}.linux-amd64.tar.gz

go build -o ${name}_linux_amd64 .
"

Then, compress yourgame_linux_386 and yourgame_linux_amd64 as zips, and upload them as builds at Steamworks.

Misc.

  • I strongly recommend to adjust your account settings to enable to download Dev Comp package. You can test your application in a production-like environment by putting it at the original place where your game is downloaded by Steam (steamapps/common/yourgame).
  • You need at least 5 screenshots that are NOT titles, menus or loading. I fell into this trap.

I hope this article will help you with releasing your Ebiten games at Steam.

Steam に Ebiten ゲームをリリースする方法

Hajime Hoshi
2021-08-29

先日、 Ebiten 製ゲーム「いの べーしょん 2007!」を Steam ストアでリリースすることに成功しました。無料ゲームです。 Windows、 macOS、 Linux の全プラットフォームに対応させました。なおソースコードは GitHub にて公開されています。

Copyright 2007 Omega

Steam でゲームをリリースするためには審査が必要です。審査を通すためには、単に Ebiten ゲームを Go でビルドするだけではありません。様々な作業が必要になります。この記事では Ebiten ゲームを審査に通すために必要な事項をまとめます。 Steamworks の一般的な解説は省きます。

以下の説明では、ゲーム名を yourgame、ユーザー名を Your Name などにしています。適宜読み替えてください。

Steamworks SDK

Steam の機能にはユーザーの言語取得や実績解除などがあります。それらは Steamworks SDK 経由で呼び出されます。 SDK のファイル形式は DLL や so ファイルなどの動的ライブラリなので、 Go から使うためには一工夫必要です。

そこで、 go-steamworks というバインディングを作りました。 import するだけで使えます。例えば、「Steam クライアント経由で開いていなかったら開き直す」処理は次のように書けます。

package main

import (
	"os"

	"github.com/hajimehoshi/go-steamworks"
)

const appID = 480 // Use your application ID.

func init() {
	if steamworks.RestartAppIfNecessary(appID) {
		os.Exit(1)
	}
	if !steamworks.Init() {
		panic("steamworks.Init failed")
	}
}

なおこのバインディングはまだ API をほとんど実装していません。今後対応予定です。

Windows

Windows は最も簡単で、普通に Go でビルドするだけです。 Ebiten は Windows においては Pure Go なので、 GOOSGOARCH を指定すればどこでもビルドできます。

PowerShell でビルドする場合は次のようになります。

$Env:GOARCH = '386'
go build -o yourgame_windows_386.exe .
$Env:GOARCH = 'amd64'
go build -o yourgame_windows_amd64.exe .
Remove-Item Env:GOARCH

POSIX シェルでビルドする場合は次のようになります。

env GOOS=windows GOARCH=386 go build -o yourgame_windows_386.exe .
env GOOS=windows GOARCH=amd64 go build -o yourgame_windows_amd64.exe .

Windows 向け GUI アプリをビルドする際には -ldflags=-H=windowsgui をつけると、最初のコンソールの表示がなくなります。 Ebiten の場合はコンソールを自動で閉じるので、つけなくても問題はありません。

アイコンは必須ではありません。気になる方はリソースを埋め込むツールを適宜使ってください。

こうして出来上がった exe ファイルを zip で固めて、 Steamworks にビルドとしてアップロードしてください。

macOS

macOS の場合は .app 形式のアプリケーションを作る必要があります。また Apple から公証 (Notarization) を受ける必要があります。公証は、 Steam 審査のためには厳密には必須ではないらしいのですが、ないと新しめ (10.15 以降?) の macOS では動かなくなるので、ほぼ必須といってよいでしょう。公証を受けるためには Apple Developer の登録が必須です。

本記事作成にあたって、ブログ記事 Releasing Steam Games on Mac Is a Monster Pain を参考にしました。

アーキテクチャは amd64 を前提とします。残念ながら、 Steamworks SDK が M1 (arm64) にまだ対応していないようです。

まずアイコンファイルを icns 形式で用意します。この形式は PNG などを Preview.app で開いてエクスポートするだけで作れます。エクスポートのフォーマット一覧に出てこない場合は、 Alt キーを押しながらフォーマット一覧を開くと出てきます。

次に必要最小限の .app を次のように作ります。

name=yourgame
app_name=YourGame.app
bundle_id=com.example.yourgame

rm -rf ${app_name}
mkdir -p ${app_name}/Contents/MacOS
mkdir -p ${app_name}/Contents/Resources
env CGO_ENABLED=1 CGO_CFLAGS=-mmacosx-version-min=10.12 CGO_LDFLAGS=-mmacosx-version-min=10.12 GOARCH=amd64 go build -o ${app_name}/Contents/MacOS/${name} .
cp icon.icns ${app_name}/Contents/Resources/icon.icns
echo '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleExecutable</key>
    <string>{{.Name}}</string>
    <key>CFBundleIdentifier</key>
    <string>{{.BundleID}}</string>
    <key>CFBundleIconFile</key>
    <string>icon.icns</string>
    <key>CFBundleVersion</key>
    <string>0.0.0</string>
    <key>CFBundleShortVersionString</key>
    <string>0.0.0</string>
    <key>NSHighResolutionCapable</key>
    <true />
    <key>LSMinimumSystemVersion</key>
    <string>10.12.0</string>
  </dict>
</plist>' |
    sed -e "s/{{.Name}}/${name}/g" |
    sed -e "s/{{.BundleID}}/${bundle_id}/g" > ${app_name}/Contents/Info.plist

CGO_CFLAGSCGO_LDFLAGS-mmacosx-version-min=10.12 を指定しています。この指定がないと、ビルドした環境より古い macOS で動かなくなってしまいます。

次に App ID (Bundle ID) を、 Apple Developer の Account ページで、ない場合は作成します。

次に Developer ID (Developer ID Application) の Certificate を、 Apple Developer の Account ページで、ない場合は作成します。

次に App-Specific Password を作ります。 Apple ID のサイトから簡単に作れます。詳しくは Apple のヘルプページを参考にしてください。

次に公証を受けます。

name=yourgame
app_name=YourGame.app
bundle_id=com.example.yourgame
email=yourname@example.com
developer_name='Developer ID Application: Your Name (1234567890)'
asc_provider=1234567890

mkdir -p .cache

echo '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
  </dict>
</plist>' > .cache/entitlements.plist

codesign --display \
         --verbose \
         --verify \
         --sign "${developer_name}" \
         --timestamp \
         --options runtime \
         --force \
         --entitlements .cache/entitlements.plist \
         --deep \
         ${app_name}

ditto -c -k --keepParent ${app_name} ${app_name}.zip

if [[ -z "${APP_SPECIFIC_PASSWORD}" ]]; then
    echo 'fail: set APP_SPECIFIC_PASSWORD. See https://support.apple.com/en-us/HT204397'
    exit 1
fi

xcrun altool --notarize-app \
             --primary-bundle-id "${bundle_id}" \
             --username "${email}" \
             --password "${APP_SPECIFIC_PASSWORD}" \
             --asc-provider "${asc_provider}" \
             --file ${app_name}.zip
rm ${app_name}.zip

実行すると公証のトランザクション UUID が表示されます。 5 分くらい待つと Apple からメールが来るはずです。

公証に成功した場合は次のコマンドを実行してください。

xcrun stapler staple YourGame.app

公証のログを見たい場合は、次のコマンドを実行してください。引数は適切な値に置き換えてください。コマンドを実行すると URL が表示され、それにアクセスすると現在の状況が見れます。失敗した場合は失敗理由も書いてあるはずです。

xcrun altool --notarization-info UUID --username YOUR_MAIL_ADDRESS --password APP_SPECIFIC_PASSWORD

こうして出来上がった .app を Steamworks にビルドとしてアップロードする際、 zip コマンドや Finder のメニューで zip ファイルを作ってはいけません。公証を受けた .app には特殊なファイルをが含まれていて、普通に zip を作ろうとするとそのファイルが欠落してしまいます。代わりに ditto コマンドを使って zip ファイルを作ります。

ditto -c -k --keepParent YourGame.app yourgame_darwin_amd64.zip

Linux

Linux の場合は Steam Runtime が Dockerfile として用意されています。その環境下でビルドするのが最も簡単です。

name=yourgame
STEAM_RUNTIME_VERSION=0.20210817.0
GO_VERSION=$(go env GOVERSION)

mkdir -p .cache/${STEAM_RUNTIME_VERSION}

# Download binaries for 386.
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.Dockerfile ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.Dockerfile)
fi
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.tar.gz ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.tar.gz)
fi
if [[ ! -f .cache/${GO_VERSION}.linux-386.tar.gz ]]; then
    (cd .cache; curl --location --remote-name https://golang.org/dl/${GO_VERSION}.linux-386.tar.gz)
fi

# Download binaries for amd64.
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.Dockerfile ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.Dockerfile)
fi
if [[ ! -f .cache/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.tar.gz ]]; then
    (cd .cache/${STEAM_RUNTIME_VERSION}; curl --location --remote-name https://repo.steampowered.com/steamrt-images-scout/snapshots/${STEAM_RUNTIME_VERSION}/com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.tar.gz)
fi
if [[ ! -f .cache/${GO_VERSION}.linux-amd64.tar.gz ]]; then
    (cd .cache; curl --location --remote-name https://golang.org/dl/${GO_VERSION}.linux-amd64.tar.gz)
fi

# Build for 386
(cd .cache/${STEAM_RUNTIME_VERSION}; docker build -f com.valvesoftware.SteamRuntime.Sdk-i386-scout-sysroot.Dockerfile -t steamrt_scout_i386:latest .)
docker run --rm --workdir=/work --volume $(pwd):/work steamrt_scout_i386:latest /bin/sh -c "
export PATH=\$PATH:/usr/local/go/bin
export CGO_CFLAGS=-std=gnu99

rm -rf /usr/local/go && tar -C /usr/local -xzf .cache/${GO_VERSION}.linux-386.tar.gz

go build -o ${name}_linux_386 .
"

# Build for amd64
(cd .cache/${STEAM_RUNTIME_VERSION}; docker build -f com.valvesoftware.SteamRuntime.Sdk-amd64,i386-scout-sysroot.Dockerfile -t steamrt_scout_amd64:latest .)
docker run --rm --workdir=/work --volume $(pwd):/work steamrt_scout_amd64:latest /bin/sh -c "
export PATH=\$PATH:/usr/local/go/bin
export CGO_CFLAGS=-std=gnu99

rm -rf /usr/local/go && tar -C /usr/local -xzf .cache/${GO_VERSION}.linux-amd64.tar.gz

go build -o ${name}_linux_amd64 .
"

出来上がった yourgame_linux_386 および yourgame_linux_amd64 をそれぞれ zip に固めて、 Steamworks にビルドとしてアップロードしてください。

その他

  • Dev Comp パッケージをダウンロードできるようにアカウントを設定することを強く推奨します。 Steam でゲームがダウンロードされる本来の箇所 (steamapps/common/yourgame) で実行ファイルを置き換えることで、本番に近いテストが行なえます。
  • スクリーンショットとして、タイトル、メニュー、ローディング以外の画面が 5 枚必要です。筆者はこれにハマりました。

本記事が Ebiten ゲームの Steam リリースの一助になれば幸いです。