This utility is written in Go and uses the Fyne.io toolkit. Therefore, at the very least you need a Go compiler installed, and then you need to install the Fyne.io toolkit, which itself has a few prerequisites.
NOTE: My work is mostly done on macOS, but your setup should not be drastically different.
This is a bit of a journey, but I hope you enjoy the ride.
- Go to https://go.dev/dl/ and download the installer for your OS.
Pay attention that the installers shown in the boxes near the top are the most common ones (e.g., the Intel x64 version for MS Windows, macOS, and Linux). So if you have an ARM64-based system, look down a bit for the installers specific to your OS/architecture. (I note this mostly for macOS users who have Apple Silicon-based Macs, as otherwise by default you are installing and running the Intel x64 version under Rosetta vs. using the native version for your hardware.) - Install the Go compiler.
This may also require you to adjust your environment variables, notably PATH, to include the Gobin
directory so that when you are at the Terminal/Command Prompt/PowerShell/shell and you entergo
, it is available. - Install the Fyne.io toolkit prerequisites. From here,
Fyne requires 3 basic elements to be present, the Go tools (at least version 1.12), a C compiler (to connect with system graphics drivers) and an system graphics driver.
Now I am on macOS and already have XCode installed. YMMV. But follow that last link for more details to make sure you have the bits installed right.
Once you have that done, the key thing is to install the actual Fyne.io toolkit. This typically means entering the command
go get fyne.io/fyne/v2
But for completeness, in case you should want to develop your own Go/Fyne.io based GUI applications, the process for new Fyne apps goes something like this:
# Create directory for your app (myapp)
mkdir myapp
# Change into the directory
cd myapp
# Initialize your Go project
go mod init myapp
# Install the Fyne.io toolkit
go get fyne.io/fyne/v2
# Edit Go code in your favorite editor/IDE (I strongly recommend VSCode with the Go extension)
code .
Here you might then write a simple app such as this:
package main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myapp := app.New()
window := myapp.NewWindow("MyApp")
window.SetContent(widget.NewLabel("This is MyApp"))
window.ShowAndRun()
}
Once you save that, it's Go time. (Sorry. Couldn't resist.)
# Any time you need to clean up the config
go mod tidy
# To just run your program on the fly
go run .
# To run it with a specific theme
FYNE_THEME=dark go run .
# To compile it to 'myapp' binary (or 'myapp.exe' in Windows)
go build .
When you are ready to truly build a desktop application for distribution, Fyne has you covered. You can read more about it here: https://developer.fyne.io/started/packaging
But simply put, install the fyne
command line tool using
go install fyne.io/fyne/v2/cmd/fyne@latest
Now you can do something as simple as the following:
- Place an icon file named
Icon.png
in the same directory with your Go/Fyne source files. - Run the command
fyne package
and you will have an application for your host OS. In the case of macOS, fyne
builds a proper application bundle. For Windows, it builds a proper .exe.zip
for distribution.
Go has the ability to compile binaries for platforms other than the host OS you are running it on. This typically takes the form of modifying the environment variables GOOS
(darwin, linux, windows, android, ios) and GOARCH
(amd64, arm64). For example, I am on an Intel x64-based Mac running macOS. If I wanted to compile a Go program to a Windows .exe, I could use
GOOS=windows GOARCH=amd64 go build .
and the Go compiler would build a Windows 64-bit .exe.
Fyne.io, being written in Go, turns out to also be able to be cross-compiled as well.
HOWEVER, this can be a little tricky. For example, again, I am on macOS. As Fyne.io needs access to a graphics driver for Windows, if I try using the fyne
package directly with a command such as
fyne package -os windows -icon myicon.png
it fails due to not having everything it needs.
Now not to fret. The fine folks (sorry, did it again) at Fyne have also created a very handy program called fyne-cross
that is just one more command away:
go get github.com/fyne-io/fyne-cross
Now fyne-cross
does require that you have Docker installed and running, as it leverages Docker containers to do its work. (If you are on a Linux host doing your Go/Fyne work, you may not need this tool at all. They clearly leverage Docker as it provides a Linux environment for doing the heavy lifting.)
But if you have all the bits in place, you can now, from a single host (in my case my macOS system), build binaries for all the major platforms. To do so, you use commands such as
fyne-cross windows -name MyApp.exe -app-version 0.0.1 -app-build 1 -app-id com.example.MyApp -icon MyIcon.png -release
This one is OS-specific, so if you are not interested in macOS, just skip it. For those who are using Macs, you may know that Apple sells Macs with two different chip architectures in them: Intel x64 chips and Apple Silicon chips such as the M1, M1 Pro, M1 Max, and M1 Ultra.
Now Go compiles code down to a specific OS and architecture. This means if, like me, you are on an x64-based Mac, the Go compiler will default to building "Intel based" macOS binaries. I can adjust the GOARCH
environment variable to be arm64
, but then Go creates an "Apple Silicon" macOS binary that I can't run at all.
Go does not, natively, have the ability to create what are called "fat binaries", or Universal Binaries in the macOS world. However, there is a tool in the macOS toolchain called lipo
which can take care of this for you. That is, you can build 2 versions of a Go program--one x64 and the other ARM64--and then use lipo
to mash them together into a single Universal Binary.
Since we are using fyne
to package up our Go/Fyne applications, when working on the macOS versions, we end up not with just a single binary file of each architecture, but rather an application bundle of each. So how to actually build a Universal Binary version of a macOS application where we have 2 distinct macOS application bundles?
Well, it's actually not that hard. First, we need to understand what exactly an application bundle entails:
├── MyApp.app
│ └── Contents
│ ├── Info.plist <== This contains info such as version, build, etc.
│ ├── MacOS
│ │ └── MyApp <== This is the Go binary
│ └── Resources
│ └── icon.icns <== This is the icon file
So we build our architecture-specific application bundles and name each something like MyApp_amd64.app
and MyApp_arm64.app
. We then copy one of those application bundles over to a final MyApp.app
, which gives us the directory structure intact. Then we use lipo
against the actual, architecture-specific Go binaries located down in each of the other bundles, create a Universal Binary, and place that in the final application's MacOS
subdirectory.
And, in fact, if you look at the file buildapp.sh.example
provided in this repo, you can see how this is done. You can copy that file, adjust the variables near the top as needed, and if you are on a Mac, run this and it will build you both a Universal Binary macOS application bundle stored in a .DMG
and a Windows 64-bit application stored in a .exe.zip
, all for easy distribution.
fyne-cross windows -name MyApp.exe -app-version 0.0.1 -app-build 1 -app-id com.example.MyApp -icon MyIcon.png
GOOS=windows GOARCH=amd64 fyne package --name MyApp --appVersion 0.0.1 --appBuild 1 --appID com.example.MyApp --icon MyIcon.png --release
GOOS=darwin GOARCH=amd64 fyne package --name MyApp_amd64 --appVersion 0.0.1 --appBuild 1 --appID com.example.MyApp --icon MyIcon.png --release
GOOS=darwin GOARCH=arm64 fyne package --name MyApp_arm64 --appVersion 0.0.1 --appBuild 1 --appID com.example.MyApp --icon MyIcon.png --release
cp -R MyApp_amd64.app/ MyApp.app/
lipo -create -output ./MyApp.app/Contents/MacOS/MyApp ./MyApp_amd64.app/Contents/MacOS/MyApp ./MyApp_arm64.app/Contents/MacOS/MyApp