Dekiro Dev Blog

Part 25 of 28

Every Platform from One Windows PC

Windows, Linux, a universal macOS app, an Android APK, an iOS Xcode project and two dedicated servers, all exported from one Windows machine with Godot's command line. What each one needed, the JSON files that silently didn't ship, and how to test a build you can't click through.

How do I get an exported version of that game now?

— the developer

Seven presets #

Dekiro is developed on one Windows PC. The human doesn't own a Mac, a Linux desktop or a test phone farm, and the first build question, the one above, came on day two. Two weeks later the project has seven export presets in export_presets.cfg:

Preset Output Size (packed)
Windows (Game) Dekiro.exe + Dekiro.pck + Steam DLLs 155 MB zip
Linux (Game) Dekiro.x86_64 + .pck + Steam .so 146 MB tar.gz
macOS (Game) a universal .app (Intel + Apple Silicon), ad-hoc signed 178 MB zip
Android (Game) a debug-signed arm64 APK 147 MB
iOS (Game) an Xcode project 486 MB folder
Windows (Dedicated Server) DekiroServer.exe + .pck
Linux (Dedicated Server) DekiroServer.x86_64 + .pck

Every one of them is a single command from a shell:

godot --headless --path . --export-release "Windows (Game)" build/game/Dekiro.exe
godot --headless --path . --export-release "Linux (Game)" build/game-linux/Dekiro.x86_64
godot --headless --path . --export-release "macOS (Game)" build/game-mac/Dekiro.zip
godot --headless --path . --export-debug   "Android (Game)" build/game-android/Dekiro.apk
godot --headless --path . --export-release "iOS (Game)" build/game-ios/Dekiro.ipa   # writes an Xcode project

The sizes tell their own story. The first Windows zip, on day two, was 38 MB. The sprite characters,1 Post 12: every player and NPC is a stack of palette-indexed sprite sheets rendered from 3D characters, eight directions per clip. 177 monster models, 20 dungeons and 30 music tracks brought it to 155 MB.

Export templates, once #

Godot exports by copying a prebuilt engine binary (an export template) and appending the game's data pack. The templates weren't installed when the first build was asked for. The AI downloaded the official 4.7.2 package (1.28 GB) from Godot's GitHub releases, checked it against the published checksum, and unpacked it to %APPDATA%\Godot\export_templates\4.7.2.stable. That one package holds the templates for Windows, Linux, macOS, Android, iOS and the web, so every later platform needed no further download from Godot, only the platform's own tools.

The JSON that didn't ship #

Godot exports resources: scenes, scripts, imported textures and sounds. A plain .json file isn't a resource, so an export leaves it out unless the preset's include filter names it. Dekiro is data-driven:2 Post 2: skills, items and loot tables as data, with almost nothing hard-coded. skills, items, monsters, maps, quests and NPCs are all JSON. So the very first presets had:

include_filter="data/*.json,data/maps/*.json"
exclude_filter="tests/*,tools/*,docs/*"

The rule went into the project's rulebook ("add *.json to the export preset's non-resource include filter, or data/*.json won't ship") and it worked for four days. Then the sprite characters arrived, and each body's sprites came with an atlas.json: where every frame sits on the packed pages, where the feet and hands are, which frame the blow lands on. It lived in assets/sprites/, not data/. Nothing failed: no test ran an exported build, and the game code falls back to the old capsule when a sprite sheet can't be loaded, which is exactly what it should do headless. An exported build would have shown every player and NPC as a capsule.

It was caught before shipping, but only because a build request ("Get me new builds for Windows and Linux") was preceded by a check of the filters against what the game loads. The fix was one line in every preset:

include_filter="data/*.json,data/maps/*.json,assets/sprites/*.json"

and checking that the data files are inside the pack became part of every build since. The lesson is general. A fallback that keeps the game running also hides what's missing; for a build, a missing data file should be a loud error, or at least something a check looks for.

Windows #

The simplest target, and the one everything else is measured against. The export folder holds the exe, the .pck, and GodotSteam's two DLLs (the GDExtension and Valve's steam_api64.dll). Zip the folder, send it. Two notes for friends that went into the instructions:

  • The exe isn't code-signed, so Windows SmartScreen says "Windows protected your PC". More info, Run anyway. Signing comes with a real release.
  • During development the game uses Steam's test app 480,3 Valve's public test app id. A game uses it until it has its own Steam app id, which comes with a store page. so Steam shows everyone as playing "Spacewar".

One practical trap: the developer plays from the unpacked build/share/Dekiro-Windows folder, and a running game locks its exe and DLLs. Later builds zip straight from the export folder instead of copying over the one in use.

Linux, checked in WSL #

A Linux build needs no Linux machine to export, but it does need one to run. WSL 2 with Ubuntu is the Linux box here, with its own Godot 4.7.2 Linux binary. Two rules came out of using it:

  • Never point a Linux Godot at the Windows project folder. It would rewrite .godot/, the import cache, with Linux paths. The project is copied with rsync (excluding .godot and .git) to ~/dekiro and run there.
  • Wrap every run in timeout. A headless server that doesn't exit blocks the session; Ubuntu 26.04's timeout needs -- before the command.

The first Linux run found a real bug that Windows never showed. The exported Linux dedicated server logged 261 errors on shutdown, all "disconnect a nonexistent connection", because the multiplayer spawner was freed before the entities it had spawned. World.shutdown now frees entities first, on every quit path. The second found another: a blocked read on stdin deadlocks process exit on Linux, so the server console got a --no-console flag for services and a watchdog. The full suite, a passworded dedicated server, Linux client bots joining it, the console commands and all three ways to stop the server were verified on Ubuntu under WSL.

The packaging detail that matters for Linux: a .tar.gz, not a zip, because tar keeps the executable flag and a friend can run ./Dekiro.x86_64 straight away.

The human's next question was whether the Linux build runs on a Steam Machine. In principle yes: SteamOS is Linux on x86-64 with an AMD GPU, and a non-Steam game can be added to the library from Desktop Mode. In practice the game was keyboard and mouse only at the time, which made gamepad support the real follow-up.

macOS: universal, ad-hoc signed, and Steam #

";Can I get a Mac OS build?" took a couple of minutes of work and one project setting. The macOS preset exports a universal app (one binary with both Intel x86_64 and Apple Silicon arm64 code). Apple Silicon's GPU reads ETC2/ASTC compressed textures, not the BC formats desktop PCs use, so the importer has to write both:

[rendering]
textures/vram_compression/import_etc2_astc=true

That reimported every texture once and adds nothing to the Windows or Linux downloads, which only ship their own format.

Signing is the Mac's real hurdle. A proper release needs an Apple Developer account for a signing certificate and notarization. Without one, the preset uses ad-hoc signing (codesign/codesign=1, notarization off): the app is signed, but by nobody Apple knows. macOS blocks it on first launch, and the instructions for testers are:

  • right-click the app, Open, then Open again; after that it starts normally;
  • if macOS says the app is "damaged", run xattr -cr on it in Terminal, which removes the quarantine flag the download added.

The first Mac build also logged one error at start: the GodotSteam plugin's config pointed at addons/godotsteam/osx/, a folder that had never been added, because Steam was set up for Windows and Linux only. The game runs fine without Steam (single player, hosting and joining by address), but Mac players would have had no Steam lobbies, invites or friends list. The human asked "What's the steam on Mac thing?", got the explanation, and answered "Yea. Fix it". The fix was the matching GodotSteam 4.23.1 release's two universal .dylib files (the extension and Valve's libsteam_api.dylib), the same version the other platforms use. The export then embedded them in the app bundle with no error.

One thing nobody can check from Windows is whether it actually works on a Mac. As of this post, nobody has reported opening it on one; the first Mac player is the test.

Android and iOS #

The phone builds have their own post (19), so briefly:

  • Android builds completely on Windows, given a JDK 17 (Godot 4.7 wants 17 even though newer ones exist), the Android SDK's command-line tools with platform-tools, build-tools 35 and android-35, and a debug keystore. All three were installed into the user's folder without admin rights. The same ETC2/ASTC setting covers the textures. The result is a sideloadable APK.
  • iOS exports an Xcode project from Windows, but building, signing and installing need Xcode on a Mac. Godot won't export without an App Store Team ID in the preset, and GodotSteam (which has no iOS library) is excluded so the app doesn't try to load it. Steam is also switched off in code on both phone platforms.

Phones exclude more than the desktop builds: addons/godotsteam/* on both, and the iOS icon art on Android. Everything else uses the same include filter, which is the point: one list of data files that must ship, repeated in seven places, and checked.

The iOS icon and launch screen, drawn by a script from the game's UI art: a crimson seal with the kanji for "shadow" on the window lacquer, and the start menu's title row

Dedicated servers #

The dedicated server is the same project exported with Godot's dedicated_server feature tag, and the game checks for it (or a --server flag) to skip the menu, read server.cfg, and run with no local player. There are Windows and Linux presets. For Linux, docs/HOSTING.md includes a systemd unit whose stop command touches a STOP file in the save folder: the server notices it within a second, saves the world and exits, which is the clean way to stop a process that may have nothing attached to stdin.

During development there's a third way that needs no export at all: tools/run_dedicated_server.bat starts the server straight from the project folder with the Godot console binary.

Testing a build you can't click through #

An export that finishes without errors says little. The checks that run after one:

  1. Is the data in the pack? List the .pck and look for data/*.json, the maps and the sprite atlases.

  2. Does the exported pack run the game? Release templates refuse a scene path on the command line (you can't tell Dekiro.exe to open main.tscn directly). The editor's binary can, though, and it can load any pack:

    godot --main-pack build/game/Dekiro.pck res://scenes/main.tscn --quit-after 400
    godot --main-pack build/game/Dekiro.pck res://scenes/main.tscn -- --join=127.0.0.1:24680
    

    That runs exactly the exported data with the same engine version, smoke-tests a session, and with --join makes a client of the exported pack join a server. The exported exe itself only writes a log with --log-file.

  3. Linux in WSL, headless: the real Dekiro.x86_64 starts, and the Linux pack joins a Linux server.

  4. Mac, Android and iOS: export without errors, check the bundle's contents, and say plainly that nobody has run it yet.

A bug from the first real multiplayer session with a friend is a good reminder of where these checks stop. Joined players and their dropped loot appeared sunk into the ground on hills. The exported pack placed items correctly in single player, which pointed the search at the network code: the host stored a joined player's height as zero.4 Post 20, on the multiplayer bugs only a friend can find, tells that story. No export check would have found it; only two people on two PCs did.

Shipping it #

The builds go into build/share/ as one file per platform: Dekiro-Windows.zip, Dekiro-Linux.tar.gz (executable flag intact), Dekiro-macOS.zip, Dekiro-Android.apk. When the human asked "Are the builds now containing all changes?", the answer came from facts, not memory: the working tree is clean, no game file is newer than the exported packs, and the last commit before the export is named.

Saves never live next to the game. Godot's user:// maps to %APPDATA%\Godot\app_userdata\Dekiro- Sanctuary of Shadows\ on Windows and ~/.local/share/godot/app_userdata/... on Linux, so unpacking a new build over an old one never touches a character. (The odd "Dekiro-" comes from the project name with the colon removed, which no file system accepts.)

Takeaways #

  • One machine can export every platform; running them is the hard part. Use what you have: WSL for Linux, --main-pack for the exported data, phone-shaped windows for the touch UI.
  • Non-resource files need include filters, and a graceful fallback can hide a missing one. Check the pack, not the export log.
  • ETC2/ASTC is one setting that Apple Silicon Macs and phones both need.
  • Ad-hoc signing gets a Mac build to testers without an Apple account; tell them about right-click Open and xattr -cr.
  • Native plugins are per platform. A GDExtension with a missing .dylib or no iOS library has to be added or excluded per preset.
  • Answer "is it current?" from timestamps and git, not from what you remember building.

What's next #

The builds were heading to a friend's PC and a first playtest. One of its notes was about the minimap: it didn't show what the world actually looked like. The next post is about baking it.