From 89580f2113a190494afd7e9454bad5275beda4c6 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch
Date: Fri, 22 Oct 2021 01:59:09 +0200
Subject: sdgui: cross-compile for Windows
No one bothered to ask whether it /should/ be done.
The hamburger needs to be replaced with a file open dialog there.
---
 CMakeLists.txt                  | 105 ++++++++++++++++++++++++++++++----------
 README.adoc                     |  21 ++++++--
 cmake/FindLibIntl.cmake         |   8 +++
 cmake/Win64CrossToolchain.cmake |  15 ++++++
 cmake/Win64Depends.sh           |  74 ++++++++++++++++++++++++++++
 src/sdgui.c                     |   1 +
 src/utils.c                     |   9 +++-
 7 files changed, 203 insertions(+), 30 deletions(-)
 create mode 100644 cmake/FindLibIntl.cmake
 create mode 100644 cmake/Win64CrossToolchain.cmake
 create mode 100644 cmake/Win64Depends.sh
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d24172..43bc41e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,6 +11,25 @@ endif ()
 # For custom modules
 set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
 
+# Cross-compilation for Windows, as a proof-of-concept pulled in from logdiag
+if (WIN32)
+	if (NOT CMAKE_CROSSCOMPILING)
+		message (FATAL_ERROR "Win32 must be cross-compiled to build sensibly")
+	endif ()
+
+	set (WIN32_DEPENDS_PATH ${PROJECT_SOURCE_DIR}/win32-depends)
+	list (APPEND CMAKE_PREFIX_PATH ${WIN32_DEPENDS_PATH})
+	list (APPEND CMAKE_INCLUDE_PATH ${WIN32_DEPENDS_PATH}/lib)
+	set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mms-bitfields")
+
+	if (CMAKE_CROSSCOMPILING)
+		list (APPEND CMAKE_FIND_ROOT_PATH ${WIN32_DEPENDS_PATH})
+	endif (CMAKE_CROSSCOMPILING)
+
+	set (ENV{PKG_CONFIG_LIBDIR}
+		"${WIN32_DEPENDS_PATH}/share/pkgconfig:${WIN32_DEPENDS_PATH}/lib/pkgconfig")
+endif (WIN32)
+
 # Dependencies
 find_package (ZLIB REQUIRED)
 find_package (Ncursesw REQUIRED)
@@ -18,7 +37,7 @@ find_package (PkgConfig REQUIRED)
 pkg_check_modules (dependencies REQUIRED glib-2.0>=2.38 gio-2.0 pango)
 
 pkg_check_modules (icu icu-uc icu-i18n)
-if (NOT icu_FOUND)
+if (NOT icu_FOUND AND NOT WIN32)
 	find_program (icu_CONFIG_EXECUTABLE icu-config)
 	if (NOT icu_CONFIG_EXECUTABLE)
 		message (FATAL_ERROR "ICU not found")
@@ -73,9 +92,8 @@ if (WITH_X11)
 		message (FATAL_ERROR "XCB not found")
 	endif ()
 
-	list (APPEND dependencies_INCLUDE_DIRS ${xcb_INCLUDE_DIRS})
-	list (APPEND dependencies_LIBRARY_DIRS ${xcb_LIBRARY_DIRS})
-	list (APPEND dependencies_LIBRARIES ${xcb_LIBRARIES})
+	include_directories (${xcb_INCLUDE_DIRS})
+	link_directories (${xcb_LIBRARY_DIRS})
 endif ()
 
 pkg_check_modules (gtk gtk+-3.0)
@@ -121,6 +139,10 @@ add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
 # Project libraries
 set (project_common_libraries ${ZLIB_LIBRARIES} ${icu_LIBRARIES}
 	${dependencies_LIBRARIES})
+if (WIN32)
+	find_package (LibIntl REQUIRED)
+	list (APPEND project_common_libraries ${LibIntl_LIBRARIES})
+endif (WIN32)
 
 set (project_common_headers
 	${PROJECT_BINARY_DIR}/config.h
@@ -152,19 +174,24 @@ set (project_headers
 
 # Build the main executable and link it
 add_definitions (-DGLIB_DISABLE_DEPRECATION_WARNINGS)
-add_executable (${PROJECT_NAME}
-	${project_sources} ${project_headers} ${project_common_sources})
-target_link_libraries (${PROJECT_NAME} ${project_common_libraries}
-	${Ncursesw_LIBRARIES} termo-static)
+if (NOT WIN32)
+	add_executable (${PROJECT_NAME}
+		${project_sources} ${project_headers} ${project_common_sources})
+	target_link_libraries (${PROJECT_NAME} ${project_common_libraries}
+		${Ncursesw_LIBRARIES} termo-static)
+	if (WITH_X11)
+		target_link_libraries (${PROJECT_NAME} ${xcb_LIBRARIES})
+	endif ()
+endif (NOT WIN32)
 
 # The same for the alternative GTK+ UI
 if (WITH_GUI)
-	add_executable (sdgui
+	add_executable (sdgui WIN32
 		src/sdgui.c
 		src/stardict-view.c
 		${project_common_sources})
 	target_include_directories (sdgui PUBLIC ${gtk_INCLUDE_DIRS})
-	target_link_libraries (sdgui ${gtk_LIBRARIES} ${project_common_libraries})
+	target_link_libraries (sdgui ${project_common_libraries} ${gtk_LIBRARIES})
 endif ()
 
 # Tools
@@ -193,22 +220,50 @@ endforeach ()
 add_custom_target (dicts DEPENDS ${dicts_targets})
 
 # The files to be installed
-include (GNUInstallDirs)
-install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
-install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
-if (WITH_GUI)
-	install (TARGETS sdgui DESTINATION ${CMAKE_INSTALL_BINDIR})
-	install (FILES sdgui.desktop
-		DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
-	install (FILES sdgui.xml
-		DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages)
-endif ()
+if (NOT WIN32)
+	include (GNUInstallDirs)
+	install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
+	install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
+
+	if (WITH_GUI)
+		install (TARGETS sdgui DESTINATION ${CMAKE_INSTALL_BINDIR})
+		install (FILES sdgui.desktop
+			DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
+		install (FILES sdgui.xml
+			DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages)
+	endif ()
 
-foreach (page ${project_MAN_PAGES})
-	string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
-	install (FILES "${page}"
-		DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
-endforeach ()
+	foreach (page ${project_MAN_PAGES})
+		string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
+		install (FILES "${page}"
+			DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
+	endforeach ()
+elseif (WITH_GUI)
+	# This rather crude filter has been mostly copied over from logdiag
+	install (TARGETS sdgui DESTINATION .)
+	install (DIRECTORY
+		${WIN32_DEPENDS_PATH}/bin/
+		DESTINATION .
+		FILES_MATCHING PATTERN "*.dll"
+		PATTERN "libgettext*" EXCLUDE)
+	install (DIRECTORY
+		${WIN32_DEPENDS_PATH}/etc/
+		DESTINATION etc)
+	install (DIRECTORY
+		${WIN32_DEPENDS_PATH}/lib/gdk-pixbuf-2.0
+		DESTINATION lib
+		FILES_MATCHING PATTERN "*" PATTERN "*.a" EXCLUDE)
+	install (DIRECTORY
+		${WIN32_DEPENDS_PATH}/share/glib-2.0/schemas
+		DESTINATION share/glib-2.0)
+
+	install (DIRECTORY
+		${WIN32_DEPENDS_PATH}/share/icons/Adwaita
+		DESTINATION share/icons OPTIONAL)
+	install (FILES
+		${WIN32_DEPENDS_PATH}/share/icons/hicolor/index.theme
+		DESTINATION share/icons/hicolor)
+endif ()
 
 # Do some unit tests
 option (BUILD_TESTING "Build tests" OFF)
diff --git a/README.adoc b/README.adoc
index 05d1c9c..e81a4b0 100644
--- a/README.adoc
+++ b/README.adoc
@@ -12,9 +12,9 @@ software.
 
 image::sdtui.png[align="center"]
 
-With GTK+ 3 development packages installed, an alternative frontend will also be
-built and installed.  It shares the default dictionary list with 'sdtui',
-but styling will follow your theme, and has to be customized from 'gtk.css'.
+As a recent addition, there is now an alternative GTK+ 3 based frontend as well.
+It shares its dictionary list with 'sdtui', but styling will follow your theme,
+and may be customized from 'gtk.css'.
 
 Packages
 --------
@@ -36,7 +36,8 @@ Runtime dependencies: ncursesw, zlib, ICU, termo (included), glib-2.0 >= 2.38,
  $ git clone --recursive https://git.janouch.name/p/sdtui.git
  $ mkdir sdtui/build
  $ cd sdtui/build
- $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DWITH_X11=ON
+ $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \
+   -DWITH_X11=ON -DWITH_GUI=ON
  $ make
 
 To install the application, you can do either the usual:
@@ -53,6 +54,18 @@ an argument.  It is, however, preferable to
 link:docs/sdtui.1.adoc#_configuration[configure it] to load your dictionaries
 automatically.
 
+Windows
+~~~~~~~
+With the help of Mingw-w64 and WINE, the GUI will successfully cross-compile
+for Windows.  It isn't particularly usable on that system, if only because
+selection watching is a very X11/Wayland-specific feature.  Beware that build
+dependencies take up almost a gigabyte of disk space.
+
+ $ sh cmake/Win64Depends.cmake
+ $ cmake -DCMAKE_TOOLCHAIN_FILE=cmake/Win64CrossToolchain.cmake \
+   -DCMAKE_BUILD_TYPE=Release -B build
+ $ cmake --build build -- package
+
 Dictionaries
 ------------
 This application is intended for use with specific dictionaries: each line
diff --git a/cmake/FindLibIntl.cmake b/cmake/FindLibIntl.cmake
new file mode 100644
index 0000000..1a15555
--- /dev/null
+++ b/cmake/FindLibIntl.cmake
@@ -0,0 +1,8 @@
+# Public Domain
+
+find_library (LibIntl_LIBRARIES intl)
+
+include (FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS (LibIntl DEFAULT_MSG LibIntl_LIBRARIES)
+
+mark_as_advanced (LibIntl_LIBRARIES)
diff --git a/cmake/Win64CrossToolchain.cmake b/cmake/Win64CrossToolchain.cmake
new file mode 100644
index 0000000..68214ec
--- /dev/null
+++ b/cmake/Win64CrossToolchain.cmake
@@ -0,0 +1,15 @@
+set (CMAKE_SYSTEM_NAME "Windows")
+set (CMAKE_SYSTEM_PROCESSOR "x86_64")
+
+set (CMAKE_C_COMPILER "x86_64-w64-mingw32-gcc")
+set (CMAKE_CXX_COMPILER "x86_64-w64-mingw32-g++")
+set (CMAKE_RC_COMPILER "x86_64-w64-mingw32-windres")
+
+# Not needed to crosscompile an installation package
+#set (CMAKE_CROSSCOMPILING_EMULATOR "wine64")
+
+set (CMAKE_FIND_ROOT_PATH "/usr/x86_64-w64-mingw32")
+
+set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
diff --git a/cmake/Win64Depends.sh b/cmake/Win64Depends.sh
new file mode 100644
index 0000000..971517c
--- /dev/null
+++ b/cmake/Win64Depends.sh
@@ -0,0 +1,74 @@
+#!/bin/sh -e
+# Win64Depends.sh: download dependencies from MSYS2 for cross-compilation
+# Dependencies: AWK, sed, sha256sum, cURL, bsdtar, wine64
+repository=https://repo.msys2.org/mingw/mingw64/
+
+status() {
+	echo "$(tput bold)-- $*$(tput sgr0)"
+}
+
+dbsync() {
+	status Fetching repository DB
+	[ -f db.tsv ] || curl -# "$repository/mingw64.db" | bsdtar -xOf- | awk '
+		function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
+		NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
+		!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
+		field == "%SHA256SUM%" { path = "*packages/" f["%FILENAME%"]
+			sub(/\t$/, "", path); print $0, path > "db.sums" } END { flush() }
+	' > db.tsv
+}
+
+fetch() {
+	status Resolving "$@"
+	mkdir -p packages
+	awk -F'\t' 'function get(name,    i, a) {
+		if (visited[name]++ || !(name in filenames)) return
+		print filenames[name]; split(deps[name], a); for (i in a) get(a[i])
+	} BEGIN { while ((getline < "db.tsv") > 0) {
+		filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
+			gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
+	} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | while IFS= read -r name
+	do
+		status Fetching "$name"
+		[ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name"
+	done
+}
+
+verify() {
+	status Verifying checksums
+	sha256sum --ignore-missing --quiet -c db.sums
+}
+
+extract() {
+	status Extracting packages
+	for subdir in *
+	do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
+	done
+	for i in packages/*
+	do bsdtar -xf "$i" --strip-components 1 mingw64
+	done
+}
+
+configure() {
+	status Configuring packages
+	glib-compile-schemas share/glib-2.0/schemas
+	wine64 bin/gdk-pixbuf-query-loaders.exe \
+		> lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
+
+	# pkgconf has a command line option for this, but CMake can't pass it
+	sed -i "s|^prefix=/mingw64|prefix=$(pwd)|" {share,lib}/pkgconfig/*.pc
+}
+
+mkdir -p win32-depends
+cd win32-depends
+dbsync
+fetch mingw-w64-x86_64-gtk3 mingw-w64-x86_64-icu \
+	mingw-w64-x86_64-libwinpthread-git # because we don't do "provides"?
+verify
+extract
+configure
+
+status Success
+
+# XXX: Why is this override needed to run some GLib-based things under wine64?
+export XDG_DATA_DIRS=$(pwd)/share
diff --git a/src/sdgui.c b/src/sdgui.c
index 127f01b..5b898c7 100644
--- a/src/sdgui.c
+++ b/src/sdgui.c
@@ -20,6 +20,7 @@
 #include  
+ * Copyright (c) 2013 - 2021, Přemysl Eric Janouch  
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted.
@@ -25,7 +25,9 @@
 #include