From 14415f5ae7efd86e41889ca048b8c6b923808c3f Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Sun, 13 Apr 2025 16:12:23 +1000 Subject: [PATCH 01/13] fix typo in SupportedEditorCodec type --- frontend/src/components/CodeEditor/codec.ts | 6 +++--- .../components/PublishPanel/stores/publish-details.ts | 4 ++-- .../components/PublishPanel/stores/publish-history.ts | 4 ++-- .../Connection/DataView/stores/selected-topic-store.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/CodeEditor/codec.ts b/frontend/src/components/CodeEditor/codec.ts index e9eee51..87649d9 100644 --- a/frontend/src/components/CodeEditor/codec.ts +++ b/frontend/src/components/CodeEditor/codec.ts @@ -1,8 +1,8 @@ -export type SupportCodeEditorCodec = "none" | "base64" | "hex"; +export type SupportedCodeEditorCodec = "none" | "base64" | "hex"; export const encodePayload = ( payload: string, - codec: SupportCodeEditorCodec + codec: SupportedCodeEditorCodec ) => { if (codec === "none") { return payload; @@ -24,7 +24,7 @@ export const encodePayload = ( export const decodePayload = ( payload: string, - codec: SupportCodeEditorCodec + codec: SupportedCodeEditorCodec ) => { if (codec === "none") { return payload; diff --git a/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-details.ts b/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-details.ts index 5a726fe..48ff5aa 100644 --- a/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-details.ts +++ b/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-details.ts @@ -10,7 +10,7 @@ import { app, models, mqtt } from "wailsjs/go/models"; import type { DeepOmit } from "@/util/types"; import { encodePayload, - type SupportCodeEditorCodec, + type SupportedCodeEditorCodec, } from "@/components/CodeEditor/codec"; import type { SupportedCodeEditorFormat } from "@/components/CodeEditor/formatting"; import { emptyConvertValues } from "@/util/convertValues"; @@ -24,7 +24,7 @@ export interface PublishDetails { retain: boolean; properties: Omit; userPropertiesArray: { key: string; value: string }[]; - codec: SupportCodeEditorCodec; + codec: SupportedCodeEditorCodec; format: SupportedCodeEditorFormat; // Used to signal a call to set the contents of the editor // from payload instead of the usual reverse diff --git a/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-history.ts b/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-history.ts index 10887f1..46040b2 100644 --- a/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-history.ts +++ b/frontend/src/views/Connection/DataView/components/PublishPanel/stores/publish-history.ts @@ -8,7 +8,7 @@ import { app, models } from "wailsjs/go/models"; import type { DeepOmit } from "@/util/types"; import type { PublishDetails, PublishDetailsStore } from "./publish-details"; -import type { SupportCodeEditorCodec } from "@/components/CodeEditor/codec"; +import type { SupportedCodeEditorCodec } from "@/components/CodeEditor/codec"; import type { SupportedCodeEditorFormat } from "@/components/CodeEditor/formatting"; export type PublishHistory = DeepOmit[]; @@ -76,7 +76,7 @@ export const createPublishHistoryStore = ( qos: entry.qos, retain: entry.retain, properties: properties, - codec: entry.encoding as SupportCodeEditorCodec, + codec: entry.encoding as SupportedCodeEditorCodec, format: entry.format as SupportedCodeEditorFormat, hasAttemptedPublish: true, topicError: null, diff --git a/frontend/src/views/Connection/DataView/stores/selected-topic-store.ts b/frontend/src/views/Connection/DataView/stores/selected-topic-store.ts index 576da2f..a38887f 100644 --- a/frontend/src/views/Connection/DataView/stores/selected-topic-store.ts +++ b/frontend/src/views/Connection/DataView/stores/selected-topic-store.ts @@ -3,7 +3,7 @@ import type { mqtt } from "wailsjs/go/models"; import { GetMessageHistory } from "wailsjs/go/app/App"; import { EventsOn } from "wailsjs/runtime/runtime"; import type { events } from "wailsjs/go/models"; -import type { SupportCodeEditorCodec } from "@/components/CodeEditor/codec"; +import type { SupportedCodeEditorCodec } from "@/components/CodeEditor/codec"; import type { SupportedCodeEditorFormat } from "@/components/CodeEditor/formatting"; export type MqttHistoryMessage = Omit< @@ -21,7 +21,7 @@ interface SelectedTopicData { options: { autoSelect: boolean; compare: boolean; - decoding: SupportCodeEditorCodec; + decoding: SupportedCodeEditorCodec; format: SupportedCodeEditorFormat; }; onNewMessages: null | ((messages: MqttHistoryMessage[]) => void); From e86c9e614fd5c8c5c1d220820c35475f7ddef202 Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Sun, 13 Apr 2025 16:17:50 +1000 Subject: [PATCH 02/13] automatically pretty-print payloads if valid JSON --- .../SelectedTopicPanel.svelte | 17 ++++++++++++++++- .../components/PayloadTab.svelte | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte b/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte index 667ac24..d61b7d4 100644 --- a/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte +++ b/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte @@ -39,6 +39,21 @@ $: selectedMessagePayload = selectedMessage?.payload.toString() ?? null; $: selectedMessageRetained = selectedMessage?.retain ?? false; + $: selectedMessagePayload, + (() => { + // Auto-format to JSON if the payload is a valid JSON string + try { + if (selectedMessagePayload === null) { + return null; + } + JSON.parse(selectedMessagePayload); + // It's valid JSON + $selectedTopicStore.options.format = "json-prettier"; + } catch (e) { + // It isn't valid JSON + } + })(); + $: isComparing = $selectedTopicStore.options.compare; $: isAutoSelectingMostRecent = $selectedTopicStore.options.autoSelect; @@ -60,7 +75,7 @@ >
-
+
diff --git a/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/components/PayloadTab.svelte b/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/components/PayloadTab.svelte index 716a74d..550b3f7 100644 --- a/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/components/PayloadTab.svelte +++ b/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/components/PayloadTab.svelte @@ -4,7 +4,7 @@ import DiffCodeEditor from "@/components/CodeEditor/DiffCodeEditor.svelte"; import { decodePayload, - type SupportCodeEditorCodec, + type SupportedCodeEditorCodec, } from "@/components/CodeEditor/codec"; import { formatPayload, @@ -14,7 +14,7 @@ export let isComparing: boolean; export let payload: string; export let payloadLeftForCompare: string | null = null; - export let codec: SupportCodeEditorCodec; + export let codec: SupportedCodeEditorCodec; export let format: SupportedCodeEditorFormat; $: processPayload = (payload: string) => { From 2ae8edfe910ce89d809240a2b71012463437075f Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Sun, 13 Apr 2025 16:19:15 +1000 Subject: [PATCH 03/13] automatically use raw format for invalid JSON --- .../components/SelectedTopicPanel/SelectedTopicPanel.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte b/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte index d61b7d4..00cf9a7 100644 --- a/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte +++ b/frontend/src/views/Connection/DataView/components/SelectedTopicPanel/SelectedTopicPanel.svelte @@ -51,6 +51,7 @@ $selectedTopicStore.options.format = "json-prettier"; } catch (e) { // It isn't valid JSON + $selectedTopicStore.options.format = "none"; } })(); From 7225591bc00106160d55900b9c83eb8c01d881c6 Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Sun, 27 Apr 2025 17:00:32 +1000 Subject: [PATCH 04/13] fix protobuf files not loading --- backend/protobuf/write_files.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/protobuf/write_files.go b/backend/protobuf/write_files.go index f2172d5..24952b0 100644 --- a/backend/protobuf/write_files.go +++ b/backend/protobuf/write_files.go @@ -22,7 +22,7 @@ func WriteSparkplugProtoFiles(resourcePath string) error { // Check if the directory exists if _, err := os.Stat(protoDirPath); os.IsNotExist(err) { // Create the directory if it doesn't exist - if err := os.MkdirAll(protoDirPath, 0644); err != nil { + if err := os.MkdirAll(protoDirPath, os.ModePerm); err != nil { return err } } @@ -50,7 +50,7 @@ func writeFile(path string, bytes []byte) error { } // Write the embedded file to the specified path - if err := os.WriteFile(path, bytes, 0644); err != nil { + if err := os.WriteFile(path, bytes, os.ModePerm); err != nil { return err } return nil From b68870d167e86924783368f80b088ba6c98b2a70 Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Mon, 5 May 2025 21:27:56 +1000 Subject: [PATCH 05/13] remove local yaml definition from settings.json --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 725e50c..f3313f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,8 +7,5 @@ "a11y-click-events-have-key-events": "ignore", "a11y-no-static-element-interactions": "ignore", "a11y-no-noninteractive-element-interactions": "ignore" - }, - "yaml.schemas": { - "https://json.schemastore.org/github-issue-config.json": "file:///Users/sam/git/mqtt-viewer/.github/ISSUE_TEMPLATE/config.yml" } } From 3c710e9531eb9344b1e61cccb678eb1ffcc53928 Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Mon, 5 May 2025 21:28:04 +1000 Subject: [PATCH 06/13] ignore test certificates --- backend/security/test-certs/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/security/test-certs/.gitignore diff --git a/backend/security/test-certs/.gitignore b/backend/security/test-certs/.gitignore new file mode 100644 index 0000000..bd555ce --- /dev/null +++ b/backend/security/test-certs/.gitignore @@ -0,0 +1 @@ +viktak \ No newline at end of file From d058f28c236b53b81fd7333b6a2dfbd14749f200 Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Mon, 5 May 2025 21:28:15 +1000 Subject: [PATCH 07/13] add contribution guidelines --- CONTRIBUTING.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e04c139 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +## Contributing Guidelines + +### Start Here + +Firstly, thanks for being interested in contributing to MQTT Viewer. + +I welcome all contributions, big or small, and I really appreciate the time and effort you put into them. + +If you're not sure where to start, have a look at the [open issues](https://github.com/mqtt-viewer/mqtt-viewer/issues) and see if there's anything that you might be able to help with. + +I'm open to pair programming if you'd like to get a start on a feature or bug fix together or need help setting up your development environment. Just [open a discussion](https://github.com/mqtt-viewer/mqtt-viewer/discussions/new?category=q-a) and let me know. + +### Legal + +All contributions to this repository are made under the [MIT License](https://opensource.org/licenses/MIT). + +#### What this means practically + +Code inside your PR is automatically licensed under the permissive MIT license. This means that you are granting permission for your code to be used, modified, and distributed by anyone. + +As soon as your code is merged into the main branch, it becomes part of the project and is licensed under the same terms as the rest of the project (AGPLv3). + +#### Why this is important + +By providing your contributions under the MIT license there's no need for [CLAs](https://en.wikipedia.org/wiki/Contributor_License_Agreement) or other legal agreements should the license change in the future. + +#### MIT License for Contributions + +Copyright 2025 Code Contributor (whoever you are) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 094ae10..8efa1c4 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,50 @@ Don't see a feature that would make your life easier? [I really, really want to ## Contributing -> [!WARNING] -> The move to make this codebase open-source was a very recent decision. Considering I didn't expect this code to ever see the light of day, there's a lot of internal cleanup, dependency updates and documentation that needs to be completed before I start accepting code contributions ๐Ÿซง๐Ÿงด๐Ÿ“ - If MQTT Viewer has been helpful, right now the best ways to contribute are: - Reporting bugs and making feature requests via [GitHub issues](https://github.com/mqtt-viewer/mqtt-viewer/issues) +- Contributing to the codebase by solving bugs or implementing new features. If you're interested in contributing in this way, please [read the contributing guide](CONTRIBUTING.md) first and then choose an issue to work on! - Giving me honest, constructive feedback about what you like and don't like about MQTT Viewer via [GitHub discussions](https://github.com/mqtt-viewer/mqtt-viewer/discussions). - Seriously, nothing is too big or too small. [Let me know](https://github.com/mqtt-viewer/mqtt-viewer/issues) how to make MQTT Viewer better for you. - Letting others know about MQTT Viewer on your favourite social media or blogs. - Leaving MQTT Viewer [a testimonal!](https://testimonial.to/mqtt-viewer/) +## Development + +MQTT Viewer is built using [Wails](https://wails.io/), a Go-based application framework, and [Svelte](https://svelte.dev/). + +### Prerequisites + +- [Go](https://golang.org/doc/install) +- [Node.js](https://nodejs.org/en/download/) +- [Wails](https://wails.io/docs/gettingstarted/installation) (install via `go get github.com/wailsapp/wails/v2/cmd/wails@2.9.1`) +- [pnpm](https://pnpm.io/installation) (install via `npm install -g pnpm`) +- [Just](https://github.com/casey/just?tab=readme-ov-file#cross-platform) - optional, but recommended for running commands in the project +- [Atlas](https://github.com/ariga/atlas) - optional, only necessary if you need to create database migrations + +### Setup + +1. Clone the repository with `git clone https://github.com/mqtt-viewer/mqtt-viewer` +2. Navigate to the project directory with `cd mqtt-viewer` +3. Install the Go dependencies with `go mod tidy` +4. Navigate to the frontend directory with `cd frontend` +5. Install the Node.js dependencies with `pnpm install` +6. Navigate back to the root directory with `cd ..` +7. Run the application with `just dev` (or `wails dev` if you don't have Just installed) + +If there are problems with Wails, try running `wails doctor` to check your installation. + +Please open an issue if you have any problems. + +### Hot Reloading + +Changes to the frontend code will automatically trigger a rebuild and reload the application quickly. + +This may cause some issues if the frontend and backend are now out of sync. If so, just restart the application. + +Changes to the Go code will trigger a full rebuild which may take anywhere from a few seconds to a minute depending on your hardware specs. + ## License MQTT Viewer is open-source under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html). From 6a89ad8f8fe0b4359f5a964937c3ddef3948010c Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Mon, 5 May 2025 13:56:23 +0100 Subject: [PATCH 08/13] add appimage build commands --- build/linux/.gitignore | 3 + build/linux/MQTTViewer.desktop | 10 + .../appimage/build/linuxdeploy-plugin-gtk.sh | 376 +++++++++++ build/linux/build_appimage.go | 612 ++++++++++++++++++ build/linux/linuxdeploy-plugin-gtk.sh | 376 +++++++++++ .../mqtt-viewer_0.0.0_ARCH/DEBIAN/control | 9 - .../usr/local/bin/.gitkeep | 0 .../share/applications/mqtt-viewer.desktop | 10 - .../icons/hicolor/512x512/apps/mqttviewer.png | Bin 37360 -> 0 bytes .../share/metainfo/mqtt-viewer.appdata.xml | 28 - go.mod | 1 + go.sum | 2 + 12 files changed, 1380 insertions(+), 47 deletions(-) create mode 100644 build/linux/.gitignore create mode 100644 build/linux/MQTTViewer.desktop create mode 100755 build/linux/appimage/build/linuxdeploy-plugin-gtk.sh create mode 100644 build/linux/build_appimage.go create mode 100755 build/linux/linuxdeploy-plugin-gtk.sh delete mode 100644 build/linux/mqtt-viewer_0.0.0_ARCH/DEBIAN/control delete mode 100644 build/linux/mqtt-viewer_0.0.0_ARCH/usr/local/bin/.gitkeep delete mode 100644 build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/applications/mqtt-viewer.desktop delete mode 100644 build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/icons/hicolor/512x512/apps/mqttviewer.png delete mode 100644 build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/metainfo/mqtt-viewer.appdata.xml diff --git a/build/linux/.gitignore b/build/linux/.gitignore new file mode 100644 index 0000000..587f94d --- /dev/null +++ b/build/linux/.gitignore @@ -0,0 +1,3 @@ +appimage/build/*.AppDir +appimage/build/*.AppImage +*.AppImage \ No newline at end of file diff --git a/build/linux/MQTTViewer.desktop b/build/linux/MQTTViewer.desktop new file mode 100644 index 0000000..21e0c6d --- /dev/null +++ b/build/linux/MQTTViewer.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=MQTTViewer +Exec=MQTTViewer +Icon=appicon +Categories=Development; +Terminal=false +Keywords=wails +Version=1.0 +StartupNotify=false diff --git a/build/linux/appimage/build/linuxdeploy-plugin-gtk.sh b/build/linux/appimage/build/linuxdeploy-plugin-gtk.sh new file mode 100755 index 0000000..51c1231 --- /dev/null +++ b/build/linux/appimage/build/linuxdeploy-plugin-gtk.sh @@ -0,0 +1,376 @@ +#! /usr/bin/env bash + +# Source: https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh +# License: MIT (https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/blob/master/LICENSE.txt) + +# GTK3 environment variables: https://developer.gnome.org/gtk3/stable/gtk-running.html +# GTK4 environment variables: https://developer.gnome.org/gtk4/stable/gtk-running.html + +# abort on all errors +set -e + +if [ "$DEBUG" != "" ]; then + set -x + verbose="--verbose" +fi + +SCRIPT="$(basename "$(readlink -f "$0")")" + +show_usage() { + echo "Usage: $SCRIPT --appdir " + echo + echo "Bundles resources for applications that use GTK into an AppDir" + echo + echo "Required variables:" + echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy" + echo + echo "Optional variables:" + echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)" +} + +variable_is_true() { + local var="$1" + + if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then + return 0 # true + else + return 1 # false + fi +} + +get_pkgconf_variable() { + local variable="$1" + local library="$2" + local default_value="$3" + + pkgconfig_ret="$("$PKG_CONFIG" --variable="$variable" "$library")" + if [ -n "$pkgconfig_ret" ]; then + echo "$pkgconfig_ret" + elif [ -n "$default_value" ]; then + echo "$default_value" + else + echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr + echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr + exit 1 + fi +} + +copy_tree() { + local src=("${@:1:$#-1}") + local dst="${*:$#}" + + for elem in "${src[@]}"; do + mkdir -p "${dst::-1}$elem" + cp "$elem" --archive --parents --target-directory="$dst" $verbose + done +} + +copy_lib_tree() { + # The source lib directory could be /usr/lib, /usr/lib64, or /usr/lib/x86_64-linux-gnu + # Therefore, when copying lib directories, we need to transform that target path + # to a consistent /usr/lib + local src=("${@:1:$#-1}") + local dst="${*:$#}" + + for elem in "${src[@]}"; do + mkdir -p "${dst::-1}${elem/$LD_GTK_LIBRARY_PATH//usr/lib}" + pushd "$LD_GTK_LIBRARY_PATH" + cp "$(realpath --relative-to="$LD_GTK_LIBRARY_PATH" "$elem")" --archive --parents --target-directory="$dst/usr/lib" $verbose + popd + done +} + +get_triplet_path() { + if command -v dpkg-architecture > /dev/null; then + echo "/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)" + fi +} + + + +search_library_path() { + PATH_ARRAY=( + "$(get_triplet_path)" + "/usr/lib64" + "/usr/lib" + ) + + for path in "${PATH_ARRAY[@]}"; do + if [ -d "$path" ]; then + echo "$path" + return 0 + fi + done +} + +search_tool() { + local tool="$1" + local directory="$2" + + if command -v "$tool"; then + return 0 + fi + + PATH_ARRAY=( + "$(get_triplet_path)/$directory/$tool" + "/usr/lib64/$directory/$tool" + "/usr/lib/$directory/$tool" + "/usr/bin/$tool" + "/usr/bin/$tool-64" + "/usr/bin/$tool-32" + ) + + for path in "${PATH_ARRAY[@]}"; do + if [ -x "$path" ]; then + echo "$path" + return 0 + fi + done +} + +DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value +APPDIR= + +while [ "$1" != "" ]; do + case "$1" in + --plugin-api-version) + echo "0" + exit 0 + ;; + --appdir) + APPDIR="$2" + shift + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Invalid argument: $1" + echo + show_usage + exit 1 + ;; + esac +done + +if [ "$APPDIR" == "" ]; then + show_usage + exit 1 +fi + +APPDIR="$(realpath "$APPDIR")" +mkdir -p "$APPDIR" + +. /etc/os-release +if [ "$ID" = "debian" ] || [ "$ID" = "ubuntu" ]; then + if ! command -v dpkg-architecture &>/dev/null; then + echo -e "$0: dpkg-architecture not found.\nInstall dpkg-dev then re-run the plugin." + exit 1 + fi +fi + +if command -v pkgconf > /dev/null; then + PKG_CONFIG="pkgconf" +elif command -v pkg-config > /dev/null; then + PKG_CONFIG="pkg-config" +else + echo "$0: pkg-config/pkgconf not found in PATH, aborting" + exit 1 +fi + +# GTK's library path *must not* have a trailing slash for later parameter substitution to work properly +LD_GTK_LIBRARY_PATH="$(realpath "${LD_GTK_LIBRARY_PATH:-$(search_library_path)}")" + +if ! command -v find &>/dev/null && ! type find &>/dev/null; then + echo -e "$0: find not found.\nInstall findutils then re-run the plugin." + exit 1 +fi + +if [ -z "$LINUXDEPLOY" ]; then + echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin." + exit 1 +fi + +gtk_versions=0 # Count major versions of GTK when auto-detect GTK version +if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then + echo "Determining which GTK version to deploy" + while IFS= read -r -d '' file; do + if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then + DEPLOY_GTK_VERSION=2 + gtk_versions="$((gtk_versions+1))" + fi + if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then + DEPLOY_GTK_VERSION=3 + gtk_versions="$((gtk_versions+1))" + fi + if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then + DEPLOY_GTK_VERSION=4 + gtk_versions="$((gtk_versions+1))" + fi + done < <(find "$APPDIR/usr/bin" -executable -type f -print0) +fi + +if [ "$gtk_versions" -gt 1 ]; then + echo "$0: can not deploy multiple GTK versions at the same time." + echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." + exit 1 +elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then + echo "$0: failed to auto-detect GTK version." + echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." + exit 1 +fi + +echo "Installing AppRun hook" +HOOKSDIR="$APPDIR/apprun-hooks" +HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh" +mkdir -p "$HOOKSDIR" +cat > "$HOOKFILE" <<\EOF +#! /usr/bin/env bash + +COLOR_SCHEME="$(dbus-send --session --dest=org.freedesktop.portal.Desktop --type=method_call --print-reply --reply-timeout=1000 /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read 'string:org.freedesktop.appearance' 'string:color-scheme' 2> /dev/null | tail -n1 | cut -b35- | cut -d' ' -f2 || printf '')" +if [ -z "$COLOR_SCHEME" ]; then + COLOR_SCHEME="$(gsettings get org.gnome.desktop.interface color-scheme 2> /dev/null || printf '')" +fi +case "$COLOR_SCHEME" in + "1"|"'prefer-dark'") GTK_THEME_VARIANT="dark";; + "2"|"'prefer-light'") GTK_THEME_VARIANT="light";; + *) GTK_THEME_VARIANT="light";; +esac +APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged) + +export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage +export GTK_DATA_PREFIX="$APPDIR" +export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken +export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland +export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib +EOF + +echo "Installing GLib schemas" +# Note: schemasdir is undefined on Ubuntu 16.04 +glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")" +copy_tree "$glib_schemasdir" "$APPDIR/" +glib-compile-schemas "$APPDIR/$glib_schemasdir" +cat >> "$HOOKFILE" <> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" + else + echo "WARNING: gtk-query-immodules-3.0 not found" + fi + if [ ! -f "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then + echo "WARNING: immodules.cache file is missing" + fi + sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" + ;; + 4) + echo "Installing GTK 4.0 modules" + gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")" + gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0" + gtk4_path="$gtk4_libdir" + copy_lib_tree "$gtk4_libdir" "$APPDIR/" + cat >> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" +else + echo "WARNING: gdk-pixbuf-query-loaders not found" +fi +if [ ! -f "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then + echo "WARNING: loaders.cache file is missing" +fi +sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" + +echo "Copying more libraries" +gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0" "$LD_GTK_LIBRARY_PATH")" +gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0" "$LD_GTK_LIBRARY_PATH")" +librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0" "$LD_GTK_LIBRARY_PATH")" +pango_libdir="$(get_pkgconf_variable "libdir" "pango" "$LD_GTK_LIBRARY_PATH")" +pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo" "$LD_GTK_LIBRARY_PATH")" +pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2" "$LD_GTK_LIBRARY_PATH")" +FIND_ARRAY=( + "$gdk_libdir" "libgdk_pixbuf-*.so*" + "$gobject_libdir" "libgobject-*.so*" + "$gio_libdir" "libgio-*.so*" + "$librsvg_libdir" "librsvg-*.so*" + "$pango_libdir" "libpango-*.so*" + "$pangocairo_libdir" "libpangocairo-*.so*" + "$pangoft2_libdir" "libpangoft2-*.so*" +) +LIBRARIES=() +for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do + directory=${FIND_ARRAY[i]} + library=${FIND_ARRAY[i+1]} + while IFS= read -r -d '' file; do + LIBRARIES+=( "--library=$file" ) + done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0) +done + +env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}" + +# Create symbolic links as a workaround +# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529 +echo "Manually setting rpath for GTK modules" +PATCH_ARRAY=( + "$gtk3_immodulesdir" + "$gtk3_printbackendsdir" + "$gdk_pixbuf_moduledir" +) +for directory in "${PATCH_ARRAY[@]}"; do + while IFS= read -r -d '' file; do + ln $verbose -sf "${file/$LD_GTK_LIBRARY_PATH\//}" "$APPDIR/usr/lib" + done < <(find "$directory" -name '*.so' -print0) +done \ No newline at end of file diff --git a/build/linux/build_appimage.go b/build/linux/build_appimage.go new file mode 100644 index 0000000..ce9a83b --- /dev/null +++ b/build/linux/build_appimage.go @@ -0,0 +1,612 @@ +// Heavily modified port of the v3 wails appimage build code +// https://github.com/wailsapp/wails/blob/v3.0.0-alpha.9/v3/internal/commands/appimage.go +// with commands copied from +// https://github.com/wailsapp/wails/blob/v3.0.0-alpha.9/v3/internal/s/s.go +package main + +import ( + "crypto/md5" + _ "embed" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/google/shlex" +) + +//go:embed linuxdeploy-plugin-gtk.sh +var gtkPlugin []byte + +type GenerateAppImageOptions struct { + Binary string `description:"The binary to package including path"` + Icon string `description:"Path to the icon"` + DesktopFile string `description:"Path to the desktop file"` + OutputDir string `description:"Path to the output directory" default:"."` + BuildDir string `description:"Path to the build directory"` +} + +func getCurrentDir() string { + _, filename, _, ok := runtime.Caller(1) + if !ok { + panic("Could not get caller information") + } + return filepath.Dir(filename) +} + +var ( + currentDir = getCurrentDir() + buildDir = path.Join(currentDir, "./appimage/build") + outputDir = path.Join(currentDir, ".") + name = "MQTTViewer" + binaryPath = "../bin/MQTTViewer" + iconPath = "./appicon.png" + desktopFilePath = "./MQTTViewer.desktop" +) + +func main() { + + // Architecture-specific variables using a map + archDetails := map[string]string{ + "arm64": "aarch64", + "amd64": "x86_64", + "x86_64": "x86_64", + } + + arch, exists := archDetails[runtime.GOARCH] + if !exists { + fmt.Printf("Unsupported architecture: %s\n", runtime.GOARCH) + os.Exit(1) + } + + appDir := filepath.Join(buildDir, fmt.Sprintf("%s-%s.AppDir", name, arch)) + + // Remove existing app directory if it exists + if _, err := os.Stat(appDir); err == nil { + err = os.RemoveAll(appDir) + if err != nil { + panic(fmt.Sprintf("failed to remove existing app directory: %s", err)) + } + } + + usrBin := filepath.Join(appDir, "usr", "bin") + MKDIR(buildDir) + MKDIR(usrBin) + COPY(binaryPath, usrBin) + CHMOD(filepath.Join(usrBin, filepath.Base(binaryPath)), 0755) + dotDirIcon := filepath.Join(appDir, ".DirIcon") + COPY(iconPath, dotDirIcon) + iconLink := filepath.Join(appDir, filepath.Base(iconPath)) + DELETE(iconLink) + SYMLINK(".DirIcon", iconLink) + COPY(desktopFilePath, appDir) + + // Download linuxdeploy and make it executable + CD(buildDir) + + // Download URLs using a map based on architecture + urls := map[string]string{ + "linuxdeploy": fmt.Sprintf("https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-%s.AppImage", arch), + "AppRun": fmt.Sprintf("https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-%s", arch), + } + + // Download necessary files concurrently + var wg sync.WaitGroup + wg.Add(2) + + go func() { + linuxdeployPath := filepath.Join(buildDir, filepath.Base(urls["linuxdeploy"])) + if !EXISTS(linuxdeployPath) { + DOWNLOAD(urls["linuxdeploy"], linuxdeployPath) + } + CHMOD(linuxdeployPath, 0755) + wg.Done() + }() + + go func() { + target := filepath.Join(appDir, "AppRun") + if !EXISTS(target) { + DOWNLOAD(urls["AppRun"], target) + } + CHMOD(target, 0755) + wg.Done() + }() + + wg.Wait() + + // Processing GTK files + filesNeeded := []string{"WebKitWebProcess", "WebKitNetworkProcess", "libwebkit2gtkinjectedbundle.so"} + files, err := findGTKFiles(filesNeeded) + if err != nil { + fmt.Println("Error finding GTK files:", err) + os.Exit(1) + } + CD(appDir) + for _, file := range files { + targetDir := filepath.Dir(file) + if targetDir[0] == '/' { + targetDir = targetDir[1:] + } + targetDir, err = filepath.Abs(targetDir) + if err != nil { + fmt.Println("Error getting absolute path:", err) + os.Exit(1) + } + MKDIR(targetDir) + COPY(file, targetDir) + } + + // Copy GTK Plugin + err = os.WriteFile(filepath.Join(buildDir, "linuxdeploy-plugin-gtk.sh"), gtkPlugin, 0755) + if err != nil { + fmt.Println("Error writing GTK plugin:", err) + os.Exit(1) + } + + // Determine GTK Version + targetBinary := filepath.Join(appDir, "usr", "bin", binaryPath) + lddOutput, err := EXEC(fmt.Sprintf("ldd %s", targetBinary)) + if err != nil { + println(string(lddOutput)) + os.Exit(1) + } + lddString := string(lddOutput) + var DeployGtkVersion string + switch { + case CONTAINS(lddString, "libgtk-x11-2.0.so"): + DeployGtkVersion = "2" + case CONTAINS(lddString, "libgtk-3.so"): + DeployGtkVersion = "3" + case CONTAINS(lddString, "libgtk-4.so"): + DeployGtkVersion = "4" + default: + fmt.Println("Unable to determine GTK version") + os.Exit(1) + } + + // Run linuxdeploy to bundle the application + CD(buildDir) + linuxdeployAppImage := filepath.Join(buildDir, fmt.Sprintf("linuxdeploy-%s.AppImage", arch)) + + cmd := fmt.Sprintf("%s --appimage-extract-and-run --appdir %s --output appimage --plugin gtk", linuxdeployAppImage, appDir) + SETENV("DEPLOY_GTK_VERSION", DeployGtkVersion) + output, err := EXEC(cmd) + if err != nil { + println(output) + fmt.Println("Error running linuxdeploy:", err) + os.Exit(1) + } + + // Move file to output directory + targetFile := filepath.Join(buildDir, fmt.Sprintf("%s-%s.AppImage", name, arch)) + MOVE(targetFile, outputDir) + + fmt.Println("AppImage created successfully:", targetFile) + + // zipping app image + zipFile := filepath.Join(outputDir, fmt.Sprintf("%s-%s.AppImage.zip", name, arch)) + zipCmd := fmt.Sprintf("zip -r %s %s", zipFile, targetFile) + zipOutput, err := EXEC(zipCmd) + if err != nil { + println(zipOutput) + fmt.Println("Error zipping AppImage:", err) + os.Exit(1) + } + fmt.Println("AppImage zipped successfully:", zipFile) +} + +func findGTKFiles(files []string) ([]string, error) { + notFound := []string{} + found := []string{} + err := filepath.Walk("/usr/", func(path string, info os.FileInfo, err error) error { + if err != nil { + if os.IsPermission(err) { + return nil + } + return err + } + + if info.IsDir() { + return nil + } + + for _, fileName := range files { + if strings.HasSuffix(path, fileName) { + found = append(found, path) + break + } + } + + return nil + }) + if err != nil { + return nil, err + } + for _, fileName := range files { + fileFound := false + for _, foundPath := range found { + if strings.HasSuffix(foundPath, fileName) { + fileFound = true + break + } + } + if !fileFound { + notFound = append(notFound, fileName) + } + } + if len(notFound) > 0 { + return nil, errors.New("Unable to locate all required files: " + strings.Join(notFound, ", ")) + } + return found, nil +} + +func checkError(err error) { + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } +} + +// RENAME a file or directory +func RENAME(source string, target string) { + err := os.Rename(source, target) + checkError(err) +} + +// MUSTDELETE a file. +func MUSTDELETE(filename string) { + err := os.Remove(filepath.Join(CWD(), filename)) + checkError(err) +} + +// DELETE a file. +func DELETE(filename string) { + _ = os.Remove(filepath.Join(CWD(), filename)) +} + +func CONTAINS(list string, item string) bool { + result := strings.Contains(list, item) + listTrimmed := list + if len(listTrimmed) > 30 { + listTrimmed = listTrimmed[:30] + "..." + } + return result +} + +func SETENV(key string, value string) { + err := os.Setenv(key, value) + checkError(err) +} + +func CD(dir string) { + err := os.Chdir(dir) + checkError(err) +} +func MKDIR(path string, mode ...os.FileMode) { + var perms os.FileMode + perms = 0755 + if len(mode) == 1 { + perms = mode[0] + } + err := os.MkdirAll(path, perms) + checkError(err) +} + +// ENDIR ensures that the path gets created if it doesn't exist +func ENDIR(path string, mode ...os.FileMode) { + var perms os.FileMode + perms = 0755 + if len(mode) == 1 { + perms = mode[0] + } + _ = os.MkdirAll(path, perms) +} + +// COPYDIR recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +// Symlinks are ignored and skipped. +// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 +func COPYDIR(src string, dst string) { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + checkError(err) + if !si.IsDir() { + checkError(fmt.Errorf("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + checkError(err) + } + if err == nil { + checkError(fmt.Errorf("destination already exists")) + } + + MKDIR(dst) + + entries, err := os.ReadDir(src) + checkError(err) + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + COPYDIR(srcPath, dstPath) + } else { + // Skip symlinks. + if entry.Type()&os.ModeSymlink != 0 { + continue + } + + COPY(srcPath, dstPath) + } + } +} + +// COPYDIR2 recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory can exist. +// Symlinks are ignored and skipped. +// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 +func COPYDIR2(src string, dst string) { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + checkError(err) + if !si.IsDir() { + checkError(fmt.Errorf("source is not a directory")) + } + + MKDIR(dst) + + entries, err := os.ReadDir(src) + checkError(err) + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + COPYDIR(srcPath, dstPath) + } else { + // Skip symlinks. + if entry.Type()&os.ModeSymlink != 0 { + continue + } + + COPY(srcPath, dstPath) + } + } +} + +func SYMLINK(source string, target string) { + // trim string to first 30 chars + var trimTarget = target + if len(trimTarget) > 30 { + trimTarget = trimTarget[:30] + "..." + } + err := os.Symlink(source, target) + checkError(err) +} + +// COPY file from source to target +func COPY(source string, target string) { + src, err := os.Open(source) + checkError(err) + defer closefile(src) + if ISDIR(target) { + target = filepath.Join(target, filepath.Base(source)) + } + d, err := os.Create(target) + checkError(err) + _, err = io.Copy(d, src) + checkError(err) +} + +// Move file from source to target +func MOVE(source string, target string) { + // If target is a directory, append the source filename + if ISDIR(target) { + target = filepath.Join(target, filepath.Base(source)) + } + err := os.Rename(source, target) + checkError(err) +} + +func CWD() string { + result, err := os.Getwd() + checkError(err) + return result +} + +func RMDIR(target string) { + err := os.RemoveAll(target) + checkError(err) +} + +func RM(target string) { + err := os.Remove(target) + checkError(err) +} + +func ECHO(message string) { + println(message) +} + +func TOUCH(filepath string) { + f, err := os.Create(filepath) + checkError(err) + closefile(f) +} + +func EXEC(command string) ([]byte, error) { + // Split input using shlex + args, err := shlex.Split(command) + checkError(err) + // Execute command + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = CWD() + cmd.Env = os.Environ() + return cmd.CombinedOutput() +} + +func CHMOD(path string, mode os.FileMode) { + err := os.Chmod(path, mode) + checkError(err) +} + +// EXISTS - Returns true if the given path exists +func EXISTS(path string) bool { + _, err := os.Lstat(path) + return err == nil +} + +// ISDIR returns true if the given directory exists +func ISDIR(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsDir() +} + +// ISDIREMPTY returns true if the given directory is empty +func ISDIREMPTY(dir string) bool { + + // CREDIT: https://stackoverflow.com/a/30708914/8325411 + f, err := os.Open(dir) + checkError(err) + defer closefile(f) + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + if err == io.EOF { + return true + } + return false +} + +// ISFILE returns true if the given file exists +func ISFILE(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + +// SUBDIRS returns a list of subdirectories for the given directory +func SUBDIRS(rootDir string) []string { + var result []string + + // Iterate root dir + err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + checkError(err) + // If we have a directory, save it + if info.IsDir() { + result = append(result, path) + } + return nil + }) + checkError(err) + return result +} + +// SAVESTRING will create a file with the given string +func SAVESTRING(filename string, data string) { + SAVEBYTES(filename, []byte(data)) +} + +// LOADSTRING returns the contents of the given filename as a string +func LOADSTRING(filename string) string { + data := LOADBYTES(filename) + return string(data) +} + +// SAVEBYTES will create a file with the given string +func SAVEBYTES(filename string, data []byte) { + err := os.WriteFile(filename, data, 0755) + checkError(err) +} + +// LOADBYTES returns the contents of the given filename as a string +func LOADBYTES(filename string) []byte { + data, err := os.ReadFile(filename) + checkError(err) + return data +} + +func closefile(f *os.File) { + err := f.Close() + checkError(err) +} + +// MD5FILE returns the md5sum of the given file +func MD5FILE(filename string) string { + f, err := os.Open(filename) + checkError(err) + defer closefile(f) + + h := md5.New() + _, err = io.Copy(h, f) + checkError(err) + + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// Sub is the substitution type +type Sub map[string]string + +// REPLACEALL replaces all substitution keys with associated values in the given file +func REPLACEALL(filename string, substitutions Sub) { + data := LOADSTRING(filename) + for old, newText := range substitutions { + data = strings.ReplaceAll(data, old, newText) + } + SAVESTRING(filename, data) +} + +func DOWNLOAD(url string, target string) { + // create HTTP client + resp, err := http.Get(url) + checkError(err) + defer resp.Body.Close() + + out, err := os.Create(target) + checkError(err) + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + checkError(err) +} + +func FINDFILES(root string, filenames ...string) []string { + var result []string + // Walk the root directory trying to find all the files + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + checkError(err) + // If we have a file, check if it is in the list + if info.Mode().IsRegular() { + for _, filename := range filenames { + if info.Name() == filename { + result = append(result, path) + } + } + } + return nil + }) + checkError(err) + return result +} diff --git a/build/linux/linuxdeploy-plugin-gtk.sh b/build/linux/linuxdeploy-plugin-gtk.sh new file mode 100755 index 0000000..51c1231 --- /dev/null +++ b/build/linux/linuxdeploy-plugin-gtk.sh @@ -0,0 +1,376 @@ +#! /usr/bin/env bash + +# Source: https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh +# License: MIT (https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/blob/master/LICENSE.txt) + +# GTK3 environment variables: https://developer.gnome.org/gtk3/stable/gtk-running.html +# GTK4 environment variables: https://developer.gnome.org/gtk4/stable/gtk-running.html + +# abort on all errors +set -e + +if [ "$DEBUG" != "" ]; then + set -x + verbose="--verbose" +fi + +SCRIPT="$(basename "$(readlink -f "$0")")" + +show_usage() { + echo "Usage: $SCRIPT --appdir " + echo + echo "Bundles resources for applications that use GTK into an AppDir" + echo + echo "Required variables:" + echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy" + echo + echo "Optional variables:" + echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)" +} + +variable_is_true() { + local var="$1" + + if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then + return 0 # true + else + return 1 # false + fi +} + +get_pkgconf_variable() { + local variable="$1" + local library="$2" + local default_value="$3" + + pkgconfig_ret="$("$PKG_CONFIG" --variable="$variable" "$library")" + if [ -n "$pkgconfig_ret" ]; then + echo "$pkgconfig_ret" + elif [ -n "$default_value" ]; then + echo "$default_value" + else + echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr + echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr + exit 1 + fi +} + +copy_tree() { + local src=("${@:1:$#-1}") + local dst="${*:$#}" + + for elem in "${src[@]}"; do + mkdir -p "${dst::-1}$elem" + cp "$elem" --archive --parents --target-directory="$dst" $verbose + done +} + +copy_lib_tree() { + # The source lib directory could be /usr/lib, /usr/lib64, or /usr/lib/x86_64-linux-gnu + # Therefore, when copying lib directories, we need to transform that target path + # to a consistent /usr/lib + local src=("${@:1:$#-1}") + local dst="${*:$#}" + + for elem in "${src[@]}"; do + mkdir -p "${dst::-1}${elem/$LD_GTK_LIBRARY_PATH//usr/lib}" + pushd "$LD_GTK_LIBRARY_PATH" + cp "$(realpath --relative-to="$LD_GTK_LIBRARY_PATH" "$elem")" --archive --parents --target-directory="$dst/usr/lib" $verbose + popd + done +} + +get_triplet_path() { + if command -v dpkg-architecture > /dev/null; then + echo "/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)" + fi +} + + + +search_library_path() { + PATH_ARRAY=( + "$(get_triplet_path)" + "/usr/lib64" + "/usr/lib" + ) + + for path in "${PATH_ARRAY[@]}"; do + if [ -d "$path" ]; then + echo "$path" + return 0 + fi + done +} + +search_tool() { + local tool="$1" + local directory="$2" + + if command -v "$tool"; then + return 0 + fi + + PATH_ARRAY=( + "$(get_triplet_path)/$directory/$tool" + "/usr/lib64/$directory/$tool" + "/usr/lib/$directory/$tool" + "/usr/bin/$tool" + "/usr/bin/$tool-64" + "/usr/bin/$tool-32" + ) + + for path in "${PATH_ARRAY[@]}"; do + if [ -x "$path" ]; then + echo "$path" + return 0 + fi + done +} + +DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value +APPDIR= + +while [ "$1" != "" ]; do + case "$1" in + --plugin-api-version) + echo "0" + exit 0 + ;; + --appdir) + APPDIR="$2" + shift + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Invalid argument: $1" + echo + show_usage + exit 1 + ;; + esac +done + +if [ "$APPDIR" == "" ]; then + show_usage + exit 1 +fi + +APPDIR="$(realpath "$APPDIR")" +mkdir -p "$APPDIR" + +. /etc/os-release +if [ "$ID" = "debian" ] || [ "$ID" = "ubuntu" ]; then + if ! command -v dpkg-architecture &>/dev/null; then + echo -e "$0: dpkg-architecture not found.\nInstall dpkg-dev then re-run the plugin." + exit 1 + fi +fi + +if command -v pkgconf > /dev/null; then + PKG_CONFIG="pkgconf" +elif command -v pkg-config > /dev/null; then + PKG_CONFIG="pkg-config" +else + echo "$0: pkg-config/pkgconf not found in PATH, aborting" + exit 1 +fi + +# GTK's library path *must not* have a trailing slash for later parameter substitution to work properly +LD_GTK_LIBRARY_PATH="$(realpath "${LD_GTK_LIBRARY_PATH:-$(search_library_path)}")" + +if ! command -v find &>/dev/null && ! type find &>/dev/null; then + echo -e "$0: find not found.\nInstall findutils then re-run the plugin." + exit 1 +fi + +if [ -z "$LINUXDEPLOY" ]; then + echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin." + exit 1 +fi + +gtk_versions=0 # Count major versions of GTK when auto-detect GTK version +if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then + echo "Determining which GTK version to deploy" + while IFS= read -r -d '' file; do + if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then + DEPLOY_GTK_VERSION=2 + gtk_versions="$((gtk_versions+1))" + fi + if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then + DEPLOY_GTK_VERSION=3 + gtk_versions="$((gtk_versions+1))" + fi + if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then + DEPLOY_GTK_VERSION=4 + gtk_versions="$((gtk_versions+1))" + fi + done < <(find "$APPDIR/usr/bin" -executable -type f -print0) +fi + +if [ "$gtk_versions" -gt 1 ]; then + echo "$0: can not deploy multiple GTK versions at the same time." + echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." + exit 1 +elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then + echo "$0: failed to auto-detect GTK version." + echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." + exit 1 +fi + +echo "Installing AppRun hook" +HOOKSDIR="$APPDIR/apprun-hooks" +HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh" +mkdir -p "$HOOKSDIR" +cat > "$HOOKFILE" <<\EOF +#! /usr/bin/env bash + +COLOR_SCHEME="$(dbus-send --session --dest=org.freedesktop.portal.Desktop --type=method_call --print-reply --reply-timeout=1000 /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read 'string:org.freedesktop.appearance' 'string:color-scheme' 2> /dev/null | tail -n1 | cut -b35- | cut -d' ' -f2 || printf '')" +if [ -z "$COLOR_SCHEME" ]; then + COLOR_SCHEME="$(gsettings get org.gnome.desktop.interface color-scheme 2> /dev/null || printf '')" +fi +case "$COLOR_SCHEME" in + "1"|"'prefer-dark'") GTK_THEME_VARIANT="dark";; + "2"|"'prefer-light'") GTK_THEME_VARIANT="light";; + *) GTK_THEME_VARIANT="light";; +esac +APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged) + +export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage +export GTK_DATA_PREFIX="$APPDIR" +export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken +export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland +export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib +EOF + +echo "Installing GLib schemas" +# Note: schemasdir is undefined on Ubuntu 16.04 +glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")" +copy_tree "$glib_schemasdir" "$APPDIR/" +glib-compile-schemas "$APPDIR/$glib_schemasdir" +cat >> "$HOOKFILE" <> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" + else + echo "WARNING: gtk-query-immodules-3.0 not found" + fi + if [ ! -f "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then + echo "WARNING: immodules.cache file is missing" + fi + sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" + ;; + 4) + echo "Installing GTK 4.0 modules" + gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")" + gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0" + gtk4_path="$gtk4_libdir" + copy_lib_tree "$gtk4_libdir" "$APPDIR/" + cat >> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" +else + echo "WARNING: gdk-pixbuf-query-loaders not found" +fi +if [ ! -f "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then + echo "WARNING: loaders.cache file is missing" +fi +sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" + +echo "Copying more libraries" +gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0" "$LD_GTK_LIBRARY_PATH")" +gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0" "$LD_GTK_LIBRARY_PATH")" +librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0" "$LD_GTK_LIBRARY_PATH")" +pango_libdir="$(get_pkgconf_variable "libdir" "pango" "$LD_GTK_LIBRARY_PATH")" +pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo" "$LD_GTK_LIBRARY_PATH")" +pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2" "$LD_GTK_LIBRARY_PATH")" +FIND_ARRAY=( + "$gdk_libdir" "libgdk_pixbuf-*.so*" + "$gobject_libdir" "libgobject-*.so*" + "$gio_libdir" "libgio-*.so*" + "$librsvg_libdir" "librsvg-*.so*" + "$pango_libdir" "libpango-*.so*" + "$pangocairo_libdir" "libpangocairo-*.so*" + "$pangoft2_libdir" "libpangoft2-*.so*" +) +LIBRARIES=() +for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do + directory=${FIND_ARRAY[i]} + library=${FIND_ARRAY[i+1]} + while IFS= read -r -d '' file; do + LIBRARIES+=( "--library=$file" ) + done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0) +done + +env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}" + +# Create symbolic links as a workaround +# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529 +echo "Manually setting rpath for GTK modules" +PATCH_ARRAY=( + "$gtk3_immodulesdir" + "$gtk3_printbackendsdir" + "$gdk_pixbuf_moduledir" +) +for directory in "${PATCH_ARRAY[@]}"; do + while IFS= read -r -d '' file; do + ln $verbose -sf "${file/$LD_GTK_LIBRARY_PATH\//}" "$APPDIR/usr/lib" + done < <(find "$directory" -name '*.so' -print0) +done \ No newline at end of file diff --git a/build/linux/mqtt-viewer_0.0.0_ARCH/DEBIAN/control b/build/linux/mqtt-viewer_0.0.0_ARCH/DEBIAN/control deleted file mode 100644 index 0b8e0f1..0000000 --- a/build/linux/mqtt-viewer_0.0.0_ARCH/DEBIAN/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: mqtt-viewer -Version: 0.0.0 -Section: base -Priority: optional -Architecture: ARCH -Depends: libgtk-3-dev, libwebkit2gtk-4.0-dev -Maintainer: Sam Webster -Homepage: https://github.com/samfweb/mqtt-viewer -Description: "TODO" diff --git a/build/linux/mqtt-viewer_0.0.0_ARCH/usr/local/bin/.gitkeep b/build/linux/mqtt-viewer_0.0.0_ARCH/usr/local/bin/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/applications/mqtt-viewer.desktop b/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/applications/mqtt-viewer.desktop deleted file mode 100644 index 5ee06a1..0000000 --- a/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/applications/mqtt-viewer.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Name=MQTT Viewer -Exec=/usr/local/bin/mqtt-viewer %U -Terminal=false -Type=Application -Icon=mqtt-viewer -StartupWMClass=mqtt-viewer -Comment=An MQTT visualisation and debugging tool -MimeType=x-scheme-handler/mqtt-viewer; -Categories=Office; diff --git a/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/icons/hicolor/512x512/apps/mqttviewer.png b/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/icons/hicolor/512x512/apps/mqttviewer.png deleted file mode 100644 index 54a373d654f1ca66084abc236387711543d88d59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37360 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 diff --git a/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/metainfo/mqtt-viewer.appdata.xml b/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/metainfo/mqtt-viewer.appdata.xml deleted file mode 100644 index 8875888..0000000 --- a/build/linux/mqtt-viewer_0.0.0_ARCH/usr/share/metainfo/mqtt-viewer.appdata.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - net.utf9k.october - CC0-1.0 - GPL - Sam Webster - mqtt.viewer@gmail.com - https://github.com/mqtt-viewer/mqtt-viewer - https://github.com/mqtt-viewer/mqtt-viewer - MQTT Viewer - Visualise MQTT data - -

Visualise MQTT data - TODO

-

Community edition is open source

-
- - Office - - mqtt-viewer.desktop - mqtt-viewer - - - - - - always - -
\ No newline at end of file diff --git a/go.mod b/go.mod index 55d528e..e2ee1ad 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-resty/resty/v2 v2.14.0 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect diff --git a/go.sum b/go.sum index 2238473..38b873f 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From d2b89147ac4befc5a5ac7ec167f7aeb897efe40e Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Mon, 5 May 2025 13:58:07 +0100 Subject: [PATCH 09/13] update appimage paths --- build/linux/build_appimage.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/linux/build_appimage.go b/build/linux/build_appimage.go index ce9a83b..aed7e36 100644 --- a/build/linux/build_appimage.go +++ b/build/linux/build_appimage.go @@ -46,9 +46,9 @@ var ( buildDir = path.Join(currentDir, "./appimage/build") outputDir = path.Join(currentDir, ".") name = "MQTTViewer" - binaryPath = "../bin/MQTTViewer" - iconPath = "./appicon.png" - desktopFilePath = "./MQTTViewer.desktop" + binaryPath = path.Join(currentDir, "../bin/MQTTViewer") + iconPath = path.Join(currentDir, "../appicon.png") + desktopFilePath = path.Join(currentDir, "./MQTTViewer.desktop") ) func main() { From b2661d23bfce49e0246b389a3b29e6e9507bbb51 Mon Sep 17 00:00:00 2001 From: Sam Webster Date: Sat, 17 May 2025 05:41:42 +0100 Subject: [PATCH 10/13] specify appimage-specific update behaviour --- .github/workflows/linux-appimage-test.yaml | 112 ++++++ .github/workflows/release-linux.yaml | 2 - backend/env/env.go | 1 + backend/update/updater.go | 7 + build/linux/.gitignore | 2 +- .../appimage/build/linuxdeploy-plugin-gtk.sh | 376 ------------------ build/linux/build_appimage.go | 20 +- .../components/NotificationsButton.svelte | 5 +- 8 files changed, 135 insertions(+), 390 deletions(-) create mode 100644 .github/workflows/linux-appimage-test.yaml delete mode 100755 build/linux/appimage/build/linuxdeploy-plugin-gtk.sh diff --git a/.github/workflows/linux-appimage-test.yaml b/.github/workflows/linux-appimage-test.yaml new file mode 100644 index 0000000..d425302 --- /dev/null +++ b/.github/workflows/linux-appimage-test.yaml @@ -0,0 +1,112 @@ +name: Release MQTT Viewer - Linux + +on: + push: + tags: + - "*" + +jobs: + release: + strategy: + matrix: + build: + - arch: amd64 + platform: linux/amd64 + os: blacksmith + tag: linux_amd64 + # - arch: arm64 + # platform: linux/arm64 + # os: blacksmith-arm + # tag: linux_arm64 + runs-on: ${{ matrix.build.os }} + name: Release MQTT Viewer (${{ matrix.build.tag }}) + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + # Set up common, sanitised environment variables + + - name: Normalise version tag + id: normalise_version + shell: bash + run: | + echo "version=0.0.1-appimagetest" >> $GITHUB_OUTPUT + + - name: Define output filename + id: define_filename + shell: bash + run: | + echo "filename=MQTT_Viewer_${{ steps.normalise_version.outputs.version }}_${{ matrix.build.tag }}" >> $GITHUB_OUTPUT + + # Set up development dependencies + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.3" + + - name: Install wails + shell: bash + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.9.1 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20.15.0" + + # Dependencies + + - name: Install Ubuntu prerequisites + shell: bash + run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + + # Build + + - name: Build frontend assets + shell: bash + run: | + npm install -g pnpm + cd frontend && pnpm install + + - name: Build wails app for AppImage + shell: bash + run: | + LD_FLAGS="-X mqtt-viewer/backend/env.Version=${{ steps.normalise_version.outputs.version }}" + LD_FLAGS="${LD_FLAGS} -X mqtt-viewer/backend/env.MachineIdProtectString=${{ secrets.MACHINE_ID_SECRET }}" + LD_FLAGS="${LD_FLAGS} -X mqtt-viewer/backend/env.CloudUsername=${{ secrets.CLOUD_USERNAME }}" + LD_FLAGS="${LD_FLAGS} -X mqtt-viewer/backend/env.CloudPassword=${{ secrets.CLOUD_PASSWORD }}" + LD_FLAGS="${LD_FLAGS} -X mqtt-viewer/backend/env.IsAppImage=true" + wails build -platform ${{ matrix.build.platform }} -ldflags "${LD_FLAGS}" -tags webkit2_41 + + # Rename + + - name: Rename linux binary to be appimage compatible + shell: bash + run: | + cd build/bin && mv "MQTT Viewer" MQTTViewer + + # Package AppImage + - name: Package app image + shell: bash + run: | + cd build/linux && go run build_appimage.go + + # Rename + - name: Rename app image + shell: bash + run: | + cd build/linux && mv MQTTViewer*.AppImage ${{ steps.define_filename.outputs.filename }}.AppImage + + # ls to check it worked + - name: List files + shell: bash + run: | + ls -la build/linux + + #Publish + - name: Upload release appimage + uses: alexellis/upload-assets@0.4.1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + asset_paths: '["build/linux/${{ steps.define_filename.outputs.filename }}.AppImage"]' diff --git a/.github/workflows/release-linux.yaml b/.github/workflows/release-linux.yaml index e924e67..d823794 100644 --- a/.github/workflows/release-linux.yaml +++ b/.github/workflows/release-linux.yaml @@ -61,7 +61,6 @@ jobs: # Dependencies - name: Install Ubuntu prerequisites - if: runner.os == 'Linux' shell: bash run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev @@ -85,7 +84,6 @@ jobs: # Packaging - name: Compress linux binary - if: runner.os == 'Linux' shell: bash run: | cd build/bin && zip -r ${{ steps.define_filename.outputs.filename }}.zip "MQTT Viewer" diff --git a/backend/env/env.go b/backend/env/env.go index 235914e..4a1539f 100644 --- a/backend/env/env.go +++ b/backend/env/env.go @@ -15,6 +15,7 @@ var ( MachineId = "" CloudUsername = "dev-username" CloudPassword = "dev-password" + IsAppImage = "false" ) func init() { diff --git a/backend/update/updater.go b/backend/update/updater.go index a8deb10..5c71e28 100644 --- a/backend/update/updater.go +++ b/backend/update/updater.go @@ -47,6 +47,13 @@ func (u *Updater) CheckForUpdate() (*UpdateResponse, error) { u.updateResponse = updateResponse + isAppimage := env.IsAppImage == "true" + if isAppimage { + u.updateResponse.UpdateUrl = "" + u.updateResponse.NotificationUrl = "https://github.com/mqtt-viewer/mqtt-viewer/releases" + u.updateResponse.NotificationText = "A new update is available via AppImage. Click here to download." + } + if updateResponse.LatestVersion == env.Version || strings.TrimPrefix(updateResponse.LatestVersion, "v") == env.Version { slog.InfoContext(u.logCtx, "current version is the latest") return nil, nil diff --git a/build/linux/.gitignore b/build/linux/.gitignore index 587f94d..c462f64 100644 --- a/build/linux/.gitignore +++ b/build/linux/.gitignore @@ -1,3 +1,3 @@ appimage/build/*.AppDir appimage/build/*.AppImage -*.AppImage \ No newline at end of file +**.AppImage \ No newline at end of file diff --git a/build/linux/appimage/build/linuxdeploy-plugin-gtk.sh b/build/linux/appimage/build/linuxdeploy-plugin-gtk.sh deleted file mode 100755 index 51c1231..0000000 --- a/build/linux/appimage/build/linuxdeploy-plugin-gtk.sh +++ /dev/null @@ -1,376 +0,0 @@ -#! /usr/bin/env bash - -# Source: https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh -# License: MIT (https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/blob/master/LICENSE.txt) - -# GTK3 environment variables: https://developer.gnome.org/gtk3/stable/gtk-running.html -# GTK4 environment variables: https://developer.gnome.org/gtk4/stable/gtk-running.html - -# abort on all errors -set -e - -if [ "$DEBUG" != "" ]; then - set -x - verbose="--verbose" -fi - -SCRIPT="$(basename "$(readlink -f "$0")")" - -show_usage() { - echo "Usage: $SCRIPT --appdir " - echo - echo "Bundles resources for applications that use GTK into an AppDir" - echo - echo "Required variables:" - echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy" - echo - echo "Optional variables:" - echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)" -} - -variable_is_true() { - local var="$1" - - if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then - return 0 # true - else - return 1 # false - fi -} - -get_pkgconf_variable() { - local variable="$1" - local library="$2" - local default_value="$3" - - pkgconfig_ret="$("$PKG_CONFIG" --variable="$variable" "$library")" - if [ -n "$pkgconfig_ret" ]; then - echo "$pkgconfig_ret" - elif [ -n "$default_value" ]; then - echo "$default_value" - else - echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr - echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr - exit 1 - fi -} - -copy_tree() { - local src=("${@:1:$#-1}") - local dst="${*:$#}" - - for elem in "${src[@]}"; do - mkdir -p "${dst::-1}$elem" - cp "$elem" --archive --parents --target-directory="$dst" $verbose - done -} - -copy_lib_tree() { - # The source lib directory could be /usr/lib, /usr/lib64, or /usr/lib/x86_64-linux-gnu - # Therefore, when copying lib directories, we need to transform that target path - # to a consistent /usr/lib - local src=("${@:1:$#-1}") - local dst="${*:$#}" - - for elem in "${src[@]}"; do - mkdir -p "${dst::-1}${elem/$LD_GTK_LIBRARY_PATH//usr/lib}" - pushd "$LD_GTK_LIBRARY_PATH" - cp "$(realpath --relative-to="$LD_GTK_LIBRARY_PATH" "$elem")" --archive --parents --target-directory="$dst/usr/lib" $verbose - popd - done -} - -get_triplet_path() { - if command -v dpkg-architecture > /dev/null; then - echo "/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)" - fi -} - - - -search_library_path() { - PATH_ARRAY=( - "$(get_triplet_path)" - "/usr/lib64" - "/usr/lib" - ) - - for path in "${PATH_ARRAY[@]}"; do - if [ -d "$path" ]; then - echo "$path" - return 0 - fi - done -} - -search_tool() { - local tool="$1" - local directory="$2" - - if command -v "$tool"; then - return 0 - fi - - PATH_ARRAY=( - "$(get_triplet_path)/$directory/$tool" - "/usr/lib64/$directory/$tool" - "/usr/lib/$directory/$tool" - "/usr/bin/$tool" - "/usr/bin/$tool-64" - "/usr/bin/$tool-32" - ) - - for path in "${PATH_ARRAY[@]}"; do - if [ -x "$path" ]; then - echo "$path" - return 0 - fi - done -} - -DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value -APPDIR= - -while [ "$1" != "" ]; do - case "$1" in - --plugin-api-version) - echo "0" - exit 0 - ;; - --appdir) - APPDIR="$2" - shift - shift - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Invalid argument: $1" - echo - show_usage - exit 1 - ;; - esac -done - -if [ "$APPDIR" == "" ]; then - show_usage - exit 1 -fi - -APPDIR="$(realpath "$APPDIR")" -mkdir -p "$APPDIR" - -. /etc/os-release -if [ "$ID" = "debian" ] || [ "$ID" = "ubuntu" ]; then - if ! command -v dpkg-architecture &>/dev/null; then - echo -e "$0: dpkg-architecture not found.\nInstall dpkg-dev then re-run the plugin." - exit 1 - fi -fi - -if command -v pkgconf > /dev/null; then - PKG_CONFIG="pkgconf" -elif command -v pkg-config > /dev/null; then - PKG_CONFIG="pkg-config" -else - echo "$0: pkg-config/pkgconf not found in PATH, aborting" - exit 1 -fi - -# GTK's library path *must not* have a trailing slash for later parameter substitution to work properly -LD_GTK_LIBRARY_PATH="$(realpath "${LD_GTK_LIBRARY_PATH:-$(search_library_path)}")" - -if ! command -v find &>/dev/null && ! type find &>/dev/null; then - echo -e "$0: find not found.\nInstall findutils then re-run the plugin." - exit 1 -fi - -if [ -z "$LINUXDEPLOY" ]; then - echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin." - exit 1 -fi - -gtk_versions=0 # Count major versions of GTK when auto-detect GTK version -if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then - echo "Determining which GTK version to deploy" - while IFS= read -r -d '' file; do - if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then - DEPLOY_GTK_VERSION=2 - gtk_versions="$((gtk_versions+1))" - fi - if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then - DEPLOY_GTK_VERSION=3 - gtk_versions="$((gtk_versions+1))" - fi - if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then - DEPLOY_GTK_VERSION=4 - gtk_versions="$((gtk_versions+1))" - fi - done < <(find "$APPDIR/usr/bin" -executable -type f -print0) -fi - -if [ "$gtk_versions" -gt 1 ]; then - echo "$0: can not deploy multiple GTK versions at the same time." - echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." - exit 1 -elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then - echo "$0: failed to auto-detect GTK version." - echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." - exit 1 -fi - -echo "Installing AppRun hook" -HOOKSDIR="$APPDIR/apprun-hooks" -HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh" -mkdir -p "$HOOKSDIR" -cat > "$HOOKFILE" <<\EOF -#! /usr/bin/env bash - -COLOR_SCHEME="$(dbus-send --session --dest=org.freedesktop.portal.Desktop --type=method_call --print-reply --reply-timeout=1000 /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read 'string:org.freedesktop.appearance' 'string:color-scheme' 2> /dev/null | tail -n1 | cut -b35- | cut -d' ' -f2 || printf '')" -if [ -z "$COLOR_SCHEME" ]; then - COLOR_SCHEME="$(gsettings get org.gnome.desktop.interface color-scheme 2> /dev/null || printf '')" -fi -case "$COLOR_SCHEME" in - "1"|"'prefer-dark'") GTK_THEME_VARIANT="dark";; - "2"|"'prefer-light'") GTK_THEME_VARIANT="light";; - *) GTK_THEME_VARIANT="light";; -esac -APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged) - -export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage -export GTK_DATA_PREFIX="$APPDIR" -export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken -export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland -export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib -EOF - -echo "Installing GLib schemas" -# Note: schemasdir is undefined on Ubuntu 16.04 -glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")" -copy_tree "$glib_schemasdir" "$APPDIR/" -glib-compile-schemas "$APPDIR/$glib_schemasdir" -cat >> "$HOOKFILE" <> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" - else - echo "WARNING: gtk-query-immodules-3.0 not found" - fi - if [ ! -f "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then - echo "WARNING: immodules.cache file is missing" - fi - sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" - ;; - 4) - echo "Installing GTK 4.0 modules" - gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")" - gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0" - gtk4_path="$gtk4_libdir" - copy_lib_tree "$gtk4_libdir" "$APPDIR/" - cat >> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" -else - echo "WARNING: gdk-pixbuf-query-loaders not found" -fi -if [ ! -f "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then - echo "WARNING: loaders.cache file is missing" -fi -sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" - -echo "Copying more libraries" -gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0" "$LD_GTK_LIBRARY_PATH")" -gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0" "$LD_GTK_LIBRARY_PATH")" -librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0" "$LD_GTK_LIBRARY_PATH")" -pango_libdir="$(get_pkgconf_variable "libdir" "pango" "$LD_GTK_LIBRARY_PATH")" -pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo" "$LD_GTK_LIBRARY_PATH")" -pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2" "$LD_GTK_LIBRARY_PATH")" -FIND_ARRAY=( - "$gdk_libdir" "libgdk_pixbuf-*.so*" - "$gobject_libdir" "libgobject-*.so*" - "$gio_libdir" "libgio-*.so*" - "$librsvg_libdir" "librsvg-*.so*" - "$pango_libdir" "libpango-*.so*" - "$pangocairo_libdir" "libpangocairo-*.so*" - "$pangoft2_libdir" "libpangoft2-*.so*" -) -LIBRARIES=() -for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do - directory=${FIND_ARRAY[i]} - library=${FIND_ARRAY[i+1]} - while IFS= read -r -d '' file; do - LIBRARIES+=( "--library=$file" ) - done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0) -done - -env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}" - -# Create symbolic links as a workaround -# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529 -echo "Manually setting rpath for GTK modules" -PATCH_ARRAY=( - "$gtk3_immodulesdir" - "$gtk3_printbackendsdir" - "$gdk_pixbuf_moduledir" -) -for directory in "${PATCH_ARRAY[@]}"; do - while IFS= read -r -d '' file; do - ln $verbose -sf "${file/$LD_GTK_LIBRARY_PATH\//}" "$APPDIR/usr/lib" - done < <(find "$directory" -name '*.so' -print0) -done \ No newline at end of file diff --git a/build/linux/build_appimage.go b/build/linux/build_appimage.go index aed7e36..a757592 100644 --- a/build/linux/build_appimage.go +++ b/build/linux/build_appimage.go @@ -190,16 +190,16 @@ func main() { fmt.Println("AppImage created successfully:", targetFile) - // zipping app image - zipFile := filepath.Join(outputDir, fmt.Sprintf("%s-%s.AppImage.zip", name, arch)) - zipCmd := fmt.Sprintf("zip -r %s %s", zipFile, targetFile) - zipOutput, err := EXEC(zipCmd) - if err != nil { - println(zipOutput) - fmt.Println("Error zipping AppImage:", err) - os.Exit(1) - } - fmt.Println("AppImage zipped successfully:", zipFile) + // // zipping app image + // zipFile := filepath.Join(outputDir, fmt.Sprintf("%s-%s.AppImage.zip", name, arch)) + // zipCmd := fmt.Sprintf("zip -r %s %s", zipFile, targetFile) + // zipOutput, err := EXEC(zipCmd) + // if err != nil { + // println(zipOutput) + // fmt.Println("Error zipping AppImage:", err) + // os.Exit(1) + // } + // fmt.Println("AppImage zipped successfully:", zipFile) } func findGTKFiles(files []string) ([]string, error) { diff --git a/frontend/src/components/AppBar/components/NotificationsButton.svelte b/frontend/src/components/AppBar/components/NotificationsButton.svelte index 2bddac6..20fa6fc 100644 --- a/frontend/src/components/AppBar/components/NotificationsButton.svelte +++ b/frontend/src/components/AppBar/components/NotificationsButton.svelte @@ -36,7 +36,10 @@ {/if} {#if hasNotifications} {#each $notifications.notifications as n} -