Skip to content

Commit

Permalink
dexc-desktop: Windows builder, installer (#2635)
Browse files Browse the repository at this point in the history
* dexc-desktop: set the window icon

This sets the window icon, which is different from either the tray icon
or any icon embedded in the binary such as the windows ICOs. On
Linxu, this uses gtk_window_set_icon to set the icon from one of the
embedded (in-memory) images so it does not need the file on disk.
On Windows, it applies the icon in a resource.

This also changes to os-specific systray icons since Windows in
particular needs these to be ico files, not pngs.

This also adds Windows syso resource files for dexc-desktop, as is
already done for dexc with -systray builds. On Windows, these resources
are also used to set the window's icon with LoadImage specifying
the window's handle and resource name instead of a file name.

---------

Co-authored-by: Jonathan Chappelow <chappjc@protonmail.com>
  • Loading branch information
peterzen and chappjc committed May 14, 2024
1 parent c1136cd commit 448297d
Show file tree
Hide file tree
Showing 32 changed files with 709 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.vscode/
.vs/
*.exe
*.log
*.log.*.gz
Expand Down
4 changes: 3 additions & 1 deletion client/cmd/dexc-desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
build/
pkg/installers
certs/
pkg/windows-msi/bin
pkg/windows-msi/obj
27 changes: 22 additions & 5 deletions client/cmd/dexc-desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import (
"decred.org/dcrdex/client/webserver"
"decred.org/dcrdex/dex"
"fyne.io/systray"
"github.com/pkg/browser"
"github.com/webview/webview"
)

Expand Down Expand Up @@ -97,6 +98,11 @@ func mainCore() error {
return nil
}

// Prepare the image file for desktop notifications.
if tmpLogoPath := storeTmpLogo(); tmpLogoPath != "" {
defer os.RemoveAll(tmpLogoPath)
}

// Initialize logging.
utc := !cfg.LocalLogs
logMaker, closeLogger := app.InitLogging(cfg.LogPath, cfg.DebugLevel, cfg.LogStdout, utc)
Expand Down Expand Up @@ -374,15 +380,19 @@ func closeWindow(windowID uint32) {
}
}
log.Infof("Closing window. %d windows remain open.", remain)
cmd.Process.Kill()
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
log.Errorf("Failed to kill %v: %v", cmd, err)
}
}

func closeAllWindows() {
m := windowManager
m.Lock()
defer m.Unlock()
for windowID, cmd := range m.windows {
cmd.Process.Kill()
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
log.Errorf("Failed to kill %v: %v", cmd, err)
}
delete(m.windows, windowID)
}
}
Expand Down Expand Up @@ -416,6 +426,11 @@ func runWebview(url string) {
defer w.Destroy()
w.SetTitle(appTitle)
w.SetSize(600, 600, webview.HintMin)
if runtime.GOOS == "windows" { // windows can use icons in its resources section, or ico files
useIcon(w, "#32512") // IDI_APPLICATION, see winres.json and https://learn.microsoft.com/en-us/windows/win32/menurc/about-icons
} else {
useIconBytes(w, FavIcon) // useIcon(w, "src/dexc.png")
}

width, height := limitedWindowWidthAndHeight(int(C.display_width()), int(C.display_height()))

Expand Down Expand Up @@ -449,7 +464,7 @@ func systrayOnReady(ctx context.Context, logDirectory string, openC chan<- struc
killC chan<- os.Signal, activeState <-chan bool) {
systray.SetIcon(FavIcon)
systray.SetTitle("DEX client")
systray.SetTooltip("Self-custodial multi-wallet")
systray.SetTooltip("Self-custodial multi-wallet with atomic swap capability, by Decred.")

// TODO: Consider reworking main so we can show the icon earlier?
// mStarting := systray.AddMenuItem("Starting...", "Starting up. Please wait...")
Expand Down Expand Up @@ -489,8 +504,10 @@ func systrayOnReady(ctx context.Context, logDirectory string, openC chan<- struc
mLogs := systray.AddMenuItem("Open logs folder", "Open the folder with your DEX logs.")
go func() {
for range mLogs.ClickedCh {
log.Debug("Opening browser to log directory at", logDirURL)
runWebviewSubprocess(ctx, logDirURL)
if err := browser.OpenURL(logDirURL); err != nil {
fmt.Fprintln(os.Stderr, err) // you're actually looking for the log file, so info on stdout is warranted
log.Errorf("Unable to open log file directory: %v", err)
}
}
}()
}
Expand Down
5 changes: 5 additions & 0 deletions client/cmd/dexc-desktop/app_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ func mainCore() error {
// Filter registered assets.
asset.SetNetwork(cfg.Net)

// Prepare the image file for desktop notifications.
if tmpLogoPath := storeTmpLogo(); tmpLogoPath != "" {
defer os.RemoveAll(tmpLogoPath)
}

// Use a hidden "dexc-desktop-state" file to prevent other processes when
// dexc-desktop is already running (e.g when non-bundled version of
// dexc-desktop is executed from cmd and vice versa).
Expand Down
10 changes: 10 additions & 0 deletions client/cmd/dexc-desktop/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

//go:generate go run github.com/tc-hib/go-winres@v0.3.1 make --in winres.json --arch "386,amd64"

package main

// After generating the rsrc_windows_amd64.syso file, it will be included in the
// binary when making an amd64 build for windows.

1 change: 1 addition & 0 deletions client/cmd/dexc-desktop/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
decred.org/dcrdex v0.6.3
fyne.io/systray v1.10.1-0.20230403195833-7dc3c09283d6
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/progrium/macdriver v0.4.0
github.com/webview/webview v0.0.0-20230415172654-8387ff8945fc
)
Expand Down
3 changes: 3 additions & 0 deletions client/cmd/dexc-desktop/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,8 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down Expand Up @@ -1431,6 +1433,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
21 changes: 21 additions & 0 deletions client/cmd/dexc-desktop/icon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

//go:build !linux && !windows && !darwin

package main

import (
_ "embed"
"github.com/webview/webview"
)

//go:embed src/dexc.png
var FavIcon []byte

//go:embed src/symbol-bw-round.png
var SymbolBWIcon []byte

func useIcon(w webview.WebView, iconPath string) { /* not supported on this platform */ }

func useIconBytes(w webview.WebView, iconBytes []byte) {}
19 changes: 19 additions & 0 deletions client/cmd/dexc-desktop/icon_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

//go:build darwin

package main

import (
_ "embed"
)

//go:embed src/dexc.png
var FavIcon []byte

//go:embed src/symbol-bw-round.png
var SymbolBWIcon []byte

// On Darwin, we instead use macdriver (not webview) and
// obj.Button().SetImage(cocoa.NSImage_InitWithData.
52 changes: 52 additions & 0 deletions client/cmd/dexc-desktop/icon_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

//go:build linux

package main

// https://docs.gtk.org/gtk3/method.Window.set_icon_from_file.html

/*
#cgo linux pkg-config: gtk+-3.0
#include <stdlib.h>
#include <gtk/gtk.h>
void use_icon_file(void *hwnd, char* iconPathC) {
gtk_window_set_icon_from_file(hwnd, iconPathC, NULL);
}
void use_icon_bytes(void *hwnd, void* iconBytes, gsize len) {
GdkPixbufLoader* loader = gdk_pixbuf_loader_new();
gdk_pixbuf_loader_write(loader, (guchar *)(iconBytes), len, NULL);
gdk_pixbuf_loader_close(loader, NULL);
GdkPixbuf* pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
gtk_window_set_icon(hwnd, pixbuf);
g_object_unref(loader);
}
*/
import "C"
import (
_ "embed"
"unsafe"

"github.com/webview/webview"
)

//go:embed src/dexc.png
var FavIcon []byte

//go:embed src/symbol-bw-round.png
var SymbolBWIcon []byte

func useIcon(w webview.WebView, iconPath string) {
iconPathC := C.CString(iconPath)
defer C.free(unsafe.Pointer(iconPathC))

C.use_icon_file(w.Window(), iconPathC)
}

func useIconBytes(w webview.WebView, iconBytes []byte) {
C.use_icon_bytes(w.Window(), unsafe.Pointer(&iconBytes[0]), C.gsize(len(iconBytes)))
}
63 changes: 63 additions & 0 deletions client/cmd/dexc-desktop/icon_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

//go:build windows

package main

/*
// This set_external_icon/set_resource_icon C implementation is originally from
// a proposed webview example:
// https://github.com/ldstein/webview/blob/2d7b6d32f5408b4a5e430711137aacd0a3791088/examples/icon-switching/main_windows.go
#include <windows.h>
void use_icon_file(const void *ptr, char* iconPath) {
HICON iconBig = LoadImage(NULL, iconPath, IMAGE_ICON, GetSystemMetrics(SM_CXICON ), GetSystemMetrics(SM_CXICON ), LR_LOADFROMFILE);
HICON iconSml = LoadImage(NULL, iconPath, IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CXSMICON), LR_LOADFROMFILE);
if (iconSml) SendMessage((HWND)ptr, WM_SETICON, ICON_SMALL, (LPARAM)iconSml);
if (iconBig) SendMessage((HWND)ptr, WM_SETICON, ICON_BIG , (LPARAM)iconBig);
}
void use_icon_from_resource(const void *ptr, char* name) {
HINSTANCE hInstance = GetModuleHandle(NULL);
HICON iconBig = (HICON)LoadImage(hInstance, name, IMAGE_ICON, GetSystemMetrics(SM_CXICON ), GetSystemMetrics(SM_CXICON ), LR_DEFAULTCOLOR);
HICON iconSml = (HICON)LoadImage(hInstance, name, IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR);
if (iconSml) SendMessage((HWND)ptr, WM_SETICON, ICON_SMALL, (LPARAM)iconSml);
if (iconBig) SendMessage((HWND)ptr, WM_SETICON, ICON_BIG , (LPARAM)iconBig);
}
*/
import "C"
import (
_ "embed"
"os"
"strings"
"unsafe"

"github.com/webview/webview"
)

//go:embed src/logo_icon_v1.ico
var FavIcon []byte

//go:embed src/symbol-bw.ico
var SymbolBWIcon []byte

func fileExists(name string) bool {
_, err := os.Stat(name)
return !os.IsNotExist(err)
}

func useIcon(w webview.WebView, iconPath string) {
iconPathC := C.CString(iconPath)
defer C.free(unsafe.Pointer(iconPathC))

if strings.HasPrefix(iconPath, "#") || !fileExists(iconPath) { // numbered or named resource in winres.json
C.use_icon_from_resource(w.Window(), iconPathC)
} else {
C.use_icon_file(w.Window(), iconPathC)
}
}

func useIconBytes(w webview.WebView, iconBytes []byte) {}
36 changes: 29 additions & 7 deletions client/cmd/dexc-desktop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"

_ "decred.org/dcrdex/client/asset/bch" // register bch asset
_ "decred.org/dcrdex/client/asset/btc" // register btc asset
Expand All @@ -77,15 +78,36 @@ const (
var (
log dex.Logger
exePath = findExePath()
srcDir = filepath.Join(filepath.Dir(exePath), "src")

//go:embed src/dexc.png
FavIcon []byte

//go:embed src/symbol-bw-round.png
SymbolBWIcon []byte
// tmpLogoPath is set to a temp file path on startup for the logo on desktop
// notifications. Do not use sendDesktopNotification until it is set.
tmpLogoPath string
)

//go:embed src/symbol-positive-gradient-256.png
var symbolAlphaPNG []byte

//go:embed src/symbol-negative-solid-256.png
var symbolSolidPNG []byte

func storeTmpLogo() (tempDir string) {
// For desktop notifications, Windows seems to be fine with a PNG file,
// unlike the window and system tray icons.
var err error
if tempDir, err = os.MkdirTemp("", "dexc"); err != nil {
fmt.Printf("Failed to make temp folder for image resources: %v\n", err)
} else if tempDir != "/" {
srcImg := FavIcon
if runtime.GOOS == "windows" {
srcImg = symbolSolidPNG // png ok, but Windows can be quirky with transparency
}
tmpLogoPath = filepath.Join(tempDir, "dexc.png")
_ = os.WriteFile(tmpLogoPath, srcImg, 0644)
// sendDesktopNotification will work now.
}
return
}

func main() {
// Wrap the actual main so defers run in it.
err := mainCore()
Expand Down Expand Up @@ -119,7 +141,7 @@ func limitedWindowWidthAndHeight(width int, height int) (int, int) {
}

func sendDesktopNotification(title, msg string) {
err := beeep.Notify(title, msg, filepath.Join(srcDir, "dexc.png"))
err := beeep.Notify(title, msg, tmpLogoPath)
if err != nil {
log.Errorf("error sending desktop notification: %v", err)
return
Expand Down
41 changes: 41 additions & 0 deletions client/cmd/dexc-desktop/pkg/Build-Windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Windows build

### Setting up the build environment

This build setup is expected to be run on a freshly installed system (e.g. in a VM), which is the best practice for building binaries for release on Windows, therefore there are no prerequisites. The setup script will install all components of the toolchain, dependencies and SDKs required to build the `dexc-desktop` binary and the MSI (MS installer), there is no need to install anything manually before running this (it can actually cause issues if different versions of the dependencies are already on the system). The builder was tested on Windows 10, Windows 11 and server equivalents, 2019/2022.

Download and run setup as Administrator (privileges are required in order to set environment variables). Open a command prompt as Administrator, and run:

```batch
cd %UserProfile%
curl -O https://raw.githubusercontent.com/decred/dcrdex/master/client/cmd/dexc-desktop/pkg/setup-windows.cmd
setup-windows.cmd <branch> <repoUrl>
```

Both `branch` and `repoUrl` are optional. If not specified, it will clone the `master` branch in the default [dcrdex repository](https://github.com/decred/dcrdex).

This will download and install `git` and `PowerShell`, clone the repo and install the build toolchain and required SDKs for the build. Once completed, close the above command prompt, open a new prompt in order to effectuate `PATH` and other environment variables configured by the setup script. Administrator privileges are NOT required for the rest of the steps.

### Running the build

```batch
cd dcrdex\client\cmd\dexc-desktop
```

Build the Windows binary:

```batch
pkg\build-windows.cmd
```

This will also build the site bundle if `client/webserver/site/dist` does not exist.

The resulting `.exe` will be in `build\windows`.

### Build the MSI (Windows Installer)

```batch
pkg\pkg-windows.cmd
```

The resulting installer binary will be located in `build\msi`.

0 comments on commit 448297d

Please sign in to comment.