├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── build.go ├── bumpversion.go ├── clean-cache.go ├── common.go ├── docker.go ├── doctor.go ├── init-plugin.go ├── init.go ├── packaging.go ├── packaging │ ├── darwin-bundle.go │ ├── darwin-dmg.go │ ├── darwin-pkg.go │ ├── linux-appimage.go │ ├── linux-deb.go │ ├── linux-pkg.go │ ├── linux-rpm.go │ ├── linux-snap.go │ ├── noop.go │ ├── packaging.go │ ├── task.go │ └── windows-msi.go ├── plugins.go ├── prepare-engine.go ├── publish-plugin.go ├── root.go ├── run.go └── version.go ├── docker └── hover-safe.sh ├── go.mod ├── go.sum ├── install-with-docker-image.sh ├── internal ├── androidmanifest │ └── android-manifest.go ├── build │ ├── binaries.go │ ├── build.go │ └── mode.go ├── config │ ├── config.go │ └── flavor.go ├── darwinhacks │ └── darwinhacks.go ├── enginecache │ ├── cache.go │ └── path.go ├── fileutils │ ├── assets │ │ ├── app │ │ │ ├── gitignore │ │ │ ├── hover.yaml.tmpl │ │ │ ├── icon.png │ │ │ ├── main.go.tmpl │ │ │ ├── main_desktop.dart │ │ │ └── options.go.tmpl │ │ ├── packaging │ │ │ ├── README.md │ │ │ ├── darwin-bundle │ │ │ │ └── Info.plist.tmpl │ │ │ ├── darwin-pkg │ │ │ │ ├── Distribution.tmpl │ │ │ │ └── PackageInfo.tmpl │ │ │ ├── linux-appimage │ │ │ │ └── AppRun.tmpl │ │ │ ├── linux-deb │ │ │ │ └── control.tmpl │ │ │ ├── linux-pkg │ │ │ │ └── PKGBUILD.tmpl │ │ │ ├── linux-rpm │ │ │ │ └── app.spec.tmpl │ │ │ ├── linux-snap │ │ │ │ └── snapcraft.yaml.tmpl │ │ │ ├── linux │ │ │ │ ├── app.desktop.tmpl │ │ │ │ └── bin.tmpl │ │ │ └── windows-msi │ │ │ │ └── app.wxs.tmpl │ │ └── plugin │ │ │ ├── README.md.dlib.tmpl │ │ │ ├── README.md.tmpl │ │ │ ├── import.go.tmpl.tmpl │ │ │ └── plugin.go.tmpl │ ├── embed.go │ └── file.go ├── log │ ├── console.go │ ├── console_windows.go │ └── log.go ├── logstreamer │ ├── LICENSE.txt │ ├── README.md │ ├── logstreamer.go │ └── logstreamer_test.go ├── modx │ ├── .fixtures │ │ └── example1 │ │ │ ├── empty.go.mod │ │ │ ├── go.mod │ │ │ └── output1.go.mod │ ├── modx.go │ └── modx_test.go ├── pubspec │ └── pubspec.go ├── version │ └── version.go └── versioncheck │ └── version.go ├── main.go └── renovate.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .idea 3 | *.iml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM snapcore/snapcraft AS snapcraft 2 | # Using multi-stage dockerfile to obtain snapcraft binary 3 | 4 | FROM ubuntu:groovy-20210723 AS flutterbuilder 5 | RUN apt-get update \ 6 | && apt-get install -y \ 7 | git curl unzip 8 | # Install Flutter from the beta channel 9 | RUN git clone --single-branch --depth=1 --branch beta https://github.com/flutter/flutter /opt/flutter 2>&1 \ 10 | && /opt/flutter/bin/flutter doctor -v 11 | 12 | FROM ubuntu:groovy-20210723 AS xarbuilder 13 | RUN apt-get update \ 14 | && apt-get install -y \ 15 | git libssl-dev libxml2-dev make g++ autoconf zlib1g-dev 16 | # Needed to patch configure.ac per https://github.com/mackyle/xar/issues/18 17 | RUN git clone --single-branch --depth=1 --branch xar-1.6.1 https://github.com/mackyle/xar 2>&1 \ 18 | && cd xar/xar \ 19 | && sed -i "s/AC_CHECK_LIB(\[crypto\], \[OpenSSL_add_all_ciphers\], , \[have_libcrypto=\"0\"\])/AC_CHECK_LIB(\[crypto\], \[OPENSSL_init_crypto\], , \[have_libcrypto=\"0\"\])/" configure.ac \ 20 | && ./autogen.sh --noconfigure \ 21 | && ./configure 2>&1 \ 22 | && make 2>&1 \ 23 | && make install 2>&1 24 | 25 | FROM ubuntu:groovy-20210723 AS bomutilsbuilder 26 | RUN apt-get update \ 27 | && apt-get install -y \ 28 | git make g++ 29 | RUN git clone --single-branch --depth=1 --branch 0.2 https://github.com/hogliux/bomutils 2>&1 \ 30 | && cd bomutils \ 31 | && make 2>&1 \ 32 | && make install 2>&1 33 | 34 | # Fixed using https://github.com/AppImage/AppImageKit/issues/828 35 | FROM ubuntu:groovy-20210723 as appimagebuilder 36 | RUN apt-get update \ 37 | && apt-get install -y \ 38 | curl 39 | RUN cd /opt \ 40 | && curl -LO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage \ 41 | && chmod a+x appimagetool-x86_64.AppImage \ 42 | && sed 's|AI\x02|\x00\x00\x00|g' -i appimagetool-x86_64.AppImage \ 43 | && ./appimagetool-x86_64.AppImage --appimage-extract \ 44 | && mv squashfs-root appimagetool 45 | 46 | # groovy ships with a too old meson version 47 | FROM ubuntu:groovy-20210723 AS pacmanbuilder 48 | ENV DEBIAN_FRONTEND=noninteractive 49 | RUN apt-get update \ 50 | && apt-get install -y \ 51 | git meson python3 python3-pip python3-setuptools python3-wheel ninja-build gcc pkg-config m4 libarchive-dev libssl-dev 52 | RUN cd /tmp \ 53 | && git clone https://git.archlinux.org/pacman.git --depth=1 --branch=v5.2.2 2>&1 \ 54 | && cd pacman \ 55 | && meson setup builddir \ 56 | && meson install -C builddir 57 | 58 | FROM dockercore/golang-cross:1.13.15 AS hover 59 | 60 | # Install dependencies via apt 61 | RUN apt-get update \ 62 | && apt-get install -y \ 63 | # dependencies for compiling linux 64 | libgl1-mesa-dev xorg-dev \ 65 | # dependencies for compiling windows 66 | wine \ 67 | # dependencies for darwin-dmg 68 | genisoimage \ 69 | # dependencies for darwin-pkg 70 | cpio git \ 71 | # dependencies for linux-rpm 72 | rpm \ 73 | # dependencies for linux-pkg 74 | fakeroot bsdtar \ 75 | # dependencies for windows-msi 76 | wixl imagemagick \ 77 | && rm -rf /var/lib/apt/lists/* 78 | 79 | COPY --from=snapcraft /snap /snap 80 | ENV PATH="/snap/bin:$PATH" 81 | ENV SNAP="/snap/snapcraft/current" 82 | ENV SNAP_NAME="snapcraft" 83 | ENV SNAP_ARCH="amd64" 84 | 85 | COPY --from=xarbuilder /usr/local/bin/xar /usr/local/bin/xar 86 | COPY --from=xarbuilder /usr/local/lib/libxar.so.1 /usr/local/lib/libxar.so.1 87 | COPY --from=xarbuilder /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 88 | 89 | COPY --from=bomutilsbuilder /usr/bin/mkbom /usr/bin/mkbom 90 | # Probably shouldn't do that, but it works and nothing breaks 91 | COPY --from=bomutilsbuilder /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /usr/lib/x86_64-linux-gnu/libstdc++.so.6 92 | 93 | COPY --from=appimagebuilder /opt/appimagetool /opt/appimagetool 94 | ENV PATH=/opt/appimagetool/usr/bin:$PATH 95 | 96 | COPY --from=pacmanbuilder /usr/bin/makepkg /usr/bin/makepkg 97 | COPY --from=pacmanbuilder /usr/bin/pacman /usr/bin/pacman 98 | COPY --from=pacmanbuilder /etc/makepkg.conf /etc/makepkg.conf 99 | COPY --from=pacmanbuilder /etc/pacman.conf /etc/pacman.conf 100 | COPY --from=pacmanbuilder /usr/share/makepkg /usr/share/makepkg 101 | COPY --from=pacmanbuilder /usr/share/pacman /usr/share/pacman 102 | COPY --from=pacmanbuilder /var/lib/pacman /var/lib/pacman 103 | COPY --from=pacmanbuilder /usr/lib/x86_64-linux-gnu/libalpm.so.12 /usr/lib/x86_64-linux-gnu/libalpm.so.12 104 | RUN ln -sf /bin/bash /usr/bin/bash 105 | RUN sed -i "s/OPTIONS=(strip /OPTIONS=(/g" /etc/makepkg.conf 106 | RUN sed -i "s/#XferCommand/XferCommand/g" /etc/pacman.conf 107 | # This makes makepkg believe we are not root. Bypassing the root check is ok, because we are in a container 108 | ENV EUID=1 109 | 110 | # Create symlink for darwin-dmg 111 | RUN ln -s $(which genisoimage) /usr/bin/mkisofs 112 | 113 | COPY --from=flutterbuilder /opt/flutter /opt/flutter 114 | RUN ln -sf /opt/flutter/bin/flutter /usr/bin/flutter 115 | 116 | # Build hover 117 | WORKDIR /go/src/app 118 | COPY . . 119 | RUN go get -d -v ./... 2>&1 120 | RUN go install -v ./... 2>&1 121 | 122 | COPY docker/hover-safe.sh /usr/local/bin/hover-safe.sh 123 | 124 | # Prepare engines 125 | ENV CGO_LDFLAGS="-L~/.cache/hover/engine/linux-release/" 126 | ENV CGO_LDFLAGS="$CGO_LDFLAGS -L~/.cache/hover/engine/linux-debug_unopt/" 127 | ENV CGO_LDFLAGS="$CGO_LDFLAGS -L~/.cache/hover/engine/linux-profile/" 128 | ENV CGO_LDFLAGS="$CGO_LDFLAGS -L~/.cache/hover/engine/windows-release/" 129 | ENV CGO_LDFLAGS="$CGO_LDFLAGS -L~/.cache/hover/engine/windows-debug_unopt/" 130 | ENV CGO_LDFLAGS="$CGO_LDFLAGS -L~/.cache/hover/engine/windows-profile/" 131 | ENV CGO_LDFLAGS="$CGO_LDFLAGS -L~/.cache/hover/engine/darwin-debug_unopt/" 132 | 133 | WORKDIR /app 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Geert-Johan Riemer 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hover - Run Flutter apps on the desktop with hot-reload 2 | 3 | Hover is a simple build tool to create [Flutter](https://flutter.dev) desktop applications. 4 | 5 | **Hover is brand new and under development, it should be considered alpha. Anything can break, please backup your data before using hover** 6 | 7 | Hover is part of the [go-flutter](https://github.com/go-flutter-desktop/go-flutter) project. Please report issues at the [go-flutter issue tracker](https://github.com/go-flutter-desktop/go-flutter/issues/). 8 | 9 | ## Install 10 | 11 | Hover uses [Go](https://golang.org) to build your Flutter application to desktop. Hover itself is also written using the Go language. You will need to [install go](https://golang.org/doc/install) on your development machine. 12 | 13 | Run `go version` and make sure that your Go version is 1.13 or higher. 14 | 15 | Then install hover by running this in your home directory: 16 | 17 | ```bash 18 | GO111MODULE=on go install github.com/go-flutter-desktop/hover@latest 19 | ``` 20 | Or windows: 21 | ``` 22 | set GO111MODULE=on 23 | go install github.com/go-flutter-desktop/hover@latest 24 | ``` 25 | Or for powershell: 26 | ```powershell 27 | $env:GO111MODULE="on"; go get -u -a github.com/go-flutter-desktop/hover@latest 28 | ``` 29 | Make sure the hover binary is on your `PATH` (defaults are `$GOPATH/bin` or `$HOME/go/bin`) 30 | 31 | Run the same command to update when a newer version becomes available. 32 | 33 | Install these dependencies: 34 | 35 | * You need to make sure you have a C compiler. 36 | The recommended C compiler are documented [here](https://github.com/golang/go/wiki/InstallFromSource#install-c-tools). 37 | 38 | * You need to make sure you have dependencies of GLFW: 39 | * On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required headers and libraries. 40 | * On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev xorg-dev` packages. 41 | * On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel` packages. 42 | * See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details. 43 | 44 | ## Getting started with an existing Flutter project 45 | 46 | This assumes you have an existing flutter project which you want to run on desktop. If you don't have a project yet, follow the flutter tutorial for setting up a new project first. 47 | 48 | ### Init project for hover 49 | 50 | cd into a flutter project. 51 | 52 | ```bash 53 | cd projects/simpleApplication 54 | ``` 55 | 56 | The first time you use hover for a project, you'll need to initialize the project for use with hover. An argument can be passed to `hover init` to set the project path. This is usually the path for your project on github or a self-hosted git service. _If you are unsure use `hover init` without a path. You can change the path later._ 57 | 58 | ```bash 59 | hover init github.com/my-organization/simpleApplication 60 | ``` 61 | 62 | This creates the directory `go` and adds boilerplate files such as Go code and a default logo. 63 | 64 | Optionally, you may add [plugins](https://github.com/go-flutter-desktop/plugins) to `go/cmd/options.go` 65 | Optionally, change the logo in `go/assets/logo.png`, which is used as icon for the window. 66 | 67 | ### Run with hot-reload 68 | 69 | To run the application and attach flutter for hot-reload support: 70 | 71 | ```bash 72 | hover run 73 | ``` 74 | 75 | The hot-reload is manual because you'll need to press 'r' in the terminal to hot-reload the application. 76 | 77 | By default, hover uses the file `lib/main_desktop.dart` as entrypoint. You may specify a different endpoint by using the `--target` flag. 78 | 79 | #### IDE integration 80 | 81 | ##### VSCode 82 | 83 | Please try the [experimental Hover extension for VSCode](https://marketplace.visualstudio.com/items?itemName=go-flutter.hover). 84 | 85 | If you want to manually integrate with VSCode, read this [issue](https://github.com/go-flutter-desktop/go-flutter/issues/129#issuecomment-513590141). 86 | 87 | ##### Emacs 88 | 89 | Check [hover.el](https://github.com/ericdallo/hover.el) packge for emacs integration. 90 | 91 | ### Build standalone application 92 | 93 | To create a standalone release (JIT mode) build run this command: 94 | 95 | ```bash 96 | hover build linux # or darwin or windows 97 | ``` 98 | 99 | You can create a build for any of the supported OSs using cross-compiling which needs [Docker to be installed](https://docs.docker.com/install/). 100 | Then run the command from above and it will do everything for you. 101 | 102 | The output will be in `go/build/outputs/linux` or windows or darwin. 103 | 104 | To start the binary: (replace `yourApplicationName` with your app name) 105 | 106 | ```bash 107 | ./go/build/outputs/linux/yourApplicationName 108 | ``` 109 | 110 | It's possible to zip the whole dir `go/build/outputs/linux` and ship it to a different machine. 111 | 112 | ### Packaging 113 | 114 | You can package your application for different packaging formats. 115 | First initialize the packaging format: 116 | 117 | ```bash 118 | hover init-packaging linux-appimage 119 | ``` 120 | 121 | Update the configuration files located in `go/packaging/linux-appimage/`to your needs. 122 | Then create a build and package it using this command: 123 | 124 | ```bash 125 | hover build linux-appimage 126 | ``` 127 | 128 | The packaging output is placed in `go/build/outputs/linux-appimage/` 129 | 130 | To get a list of all available packaging formats run: 131 | 132 | ```bash 133 | hover build --help 134 | ``` 135 | 136 | ### Flavors 137 | 138 | Hover supports different application flavors via `--flavor MY_FLAVOR` command. 139 | If you wish to create a new flavor for you application, 140 | simply copy `go/hover.yaml` into `go/hover-MY_FLAVOR.yaml` and modify contents as needed. 141 | If no flavor is specified, Hover will always default to `hover.yaml` 142 | 143 | ``` 144 | hover run --flavor develop || hover build --flavor develop 145 | // hover-develop.yaml 146 | ``` 147 | 148 | 149 | ## Issues 150 | 151 | Please report issues at the [go-flutter issue tracker](https://github.com/go-flutter-desktop/go-flutter/issues/). 152 | -------------------------------------------------------------------------------- /cmd/bumpversion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "runtime" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/go-flutter-desktop/hover/internal/build" 12 | "github.com/go-flutter-desktop/hover/internal/enginecache" 13 | "github.com/go-flutter-desktop/hover/internal/log" 14 | "github.com/go-flutter-desktop/hover/internal/versioncheck" 15 | ) 16 | 17 | func init() { 18 | upgradeCmd.Flags().StringVarP(&buildOrRunCachePath, "cache-path", "", enginecache.DefaultCachePath(), "The path that hover uses to cache dependencies such as the Flutter engine .so/.dll (defaults to the standard user cache directory)") 19 | upgradeCmd.Flags().StringVarP(&buildOrRunGoFlutterBranch, "branch", "b", "", "The 'go-flutter' version to use. (@master or @v0.20.0 for example)") 20 | rootCmd.AddCommand(upgradeCmd) 21 | } 22 | 23 | var upgradeCmd = &cobra.Command{ 24 | Use: "bumpversion", 25 | Short: "upgrade the 'go-flutter' golang library in this project", 26 | Run: func(cmd *cobra.Command, args []string) { 27 | assertInFlutterProject() 28 | // Hardcode target to the current OS (no cross-compile for this command) 29 | targetOS := runtime.GOOS 30 | 31 | err := upgrade(targetOS) 32 | if err != nil { 33 | os.Exit(1) 34 | } 35 | }, 36 | } 37 | 38 | func upgrade(targetOS string) (err error) { 39 | enginecache.ValidateOrUpdateEngine(targetOS, buildOrRunCachePath, "", build.DebugMode) 40 | return upgradeGoFlutter(targetOS) 41 | } 42 | 43 | func upgradeGoFlutter(targetOS string) (err error) { 44 | wd, err := os.Getwd() 45 | if err != nil { 46 | log.Errorf("Failed to get working dir: %v", err) 47 | return 48 | } 49 | 50 | if buildOrRunGoFlutterBranch == "" { 51 | buildOrRunGoFlutterBranch = "@latest" 52 | } 53 | 54 | cmdGoGetU := exec.Command(build.GoBin(), "get", "-u", "-d", "github.com/go-flutter-desktop/go-flutter"+buildOrRunGoFlutterBranch) 55 | cmdGoGetU.Env = append(os.Environ(), 56 | "GO111MODULE=on", 57 | ) 58 | cmdGoGetU.Dir = filepath.Join(wd, build.BuildPath) 59 | cmdGoGetU.Stderr = os.Stderr 60 | cmdGoGetU.Stdout = os.Stdout 61 | 62 | err = cmdGoGetU.Run() 63 | // When cross-compiling the command fails, but that is not an error 64 | if err != nil { 65 | log.Errorf("Updating go-flutter to %s version failed: %v", buildOrRunGoFlutterBranch, err) 66 | return 67 | } 68 | 69 | cmdGoModDownload := exec.Command(build.GoBin(), "mod", "download") 70 | cmdGoModDownload.Dir = filepath.Join(wd, build.BuildPath) 71 | cmdGoModDownload.Env = append(os.Environ(), 72 | "GO111MODULE=on", 73 | ) 74 | cmdGoModDownload.Stderr = os.Stderr 75 | cmdGoModDownload.Stdout = os.Stdout 76 | 77 | err = cmdGoModDownload.Run() 78 | if err != nil { 79 | log.Errorf("Go mod download failed: %v", err) 80 | return 81 | } 82 | 83 | currentTag, err := versioncheck.CurrentGoFlutterTag(filepath.Join(wd, build.BuildPath)) 84 | if err != nil { 85 | log.Errorf("%v", err) 86 | os.Exit(1) 87 | } 88 | 89 | log.Printf("'go-flutter' is on version: %s", currentTag) 90 | 91 | return nil 92 | 93 | } 94 | -------------------------------------------------------------------------------- /cmd/clean-cache.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/go-flutter-desktop/hover/internal/enginecache" 10 | "github.com/go-flutter-desktop/hover/internal/log" 11 | ) 12 | 13 | var cachePath string 14 | 15 | func init() { 16 | cleanCacheCmd.Flags().StringVar(&cachePath, "cache-path", enginecache.DefaultCachePath(), "The path that hover uses to cache dependencies such as the Flutter engine .so/.dll") 17 | rootCmd.AddCommand(cleanCacheCmd) 18 | } 19 | 20 | var cleanCacheCmd = &cobra.Command{ 21 | Use: "clean-cache", 22 | Short: "Clean cached engine files", 23 | Args: func(cmd *cobra.Command, args []string) error { 24 | if len(args) > 0 { 25 | return errors.New("does not take arguments") 26 | } 27 | return nil 28 | }, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | err := os.RemoveAll(enginecache.BaseEngineCachePath(cachePath)) 31 | if err != nil { 32 | log.Errorf("Failed to delete engine cache directory: %v", err) 33 | os.Exit(1) 34 | } 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "regexp" 10 | "runtime" 11 | "strings" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | 16 | "github.com/go-flutter-desktop/hover/internal/build" 17 | "github.com/go-flutter-desktop/hover/internal/log" 18 | "github.com/go-flutter-desktop/hover/internal/pubspec" 19 | "github.com/go-flutter-desktop/hover/internal/version" 20 | ) 21 | 22 | // assertInFlutterProject asserts this command is executed in a flutter project 23 | func assertInFlutterProject() { 24 | pubspec.GetPubSpec() 25 | } 26 | 27 | // assertInFlutterPluginProject asserts this command is executed in a flutter plugin project 28 | func assertInFlutterPluginProject() { 29 | if _, ok := pubspec.GetPubSpec().Flutter["plugin"]; !ok { 30 | log.Errorf("The directory doesn't appear to contain a plugin package.\nTo create a new plugin, first run `%s`, then run `%s`.", log.Au().Magenta("flutter create --template=plugin"), log.Au().Magenta("hover init-plugin")) 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | func assertHoverInitialized() { 36 | _, err := os.Stat(build.BuildPath) 37 | if os.IsNotExist(err) { 38 | if hoverMigrateDesktopToGo() { 39 | return 40 | } 41 | log.Errorf("Directory '%s' is missing. Please init go-flutter first: %s", build.BuildPath, log.Au().Magenta("hover init")) 42 | os.Exit(1) 43 | } 44 | if err != nil { 45 | log.Errorf("Failed to detect directory desktop: %v", err) 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func checkFlutterChannel() { 51 | channel := version.FlutterChannel() 52 | ignoreWarning := os.Getenv("HOVER_IGNORE_CHANNEL_WARNING") 53 | if channel != "beta" && ignoreWarning != "true" { 54 | log.Warnf("⚠ The go-flutter project tries to stay compatible with the beta channel of Flutter.") 55 | log.Warnf("⚠ It's advised to use the beta channel: `%s`", log.Au().Magenta("flutter channel beta")) 56 | } 57 | } 58 | 59 | // hoverMigrateDesktopToGo migrates from old hover buildPath directory to the new one ("desktop" -> "go") 60 | func hoverMigrateDesktopToGo() bool { 61 | oldBuildPath := "desktop" 62 | file, err := os.Open(filepath.Join(oldBuildPath, "go.mod")) 63 | if err != nil { 64 | return false 65 | } 66 | defer file.Close() 67 | 68 | log.Warnf("⚠ Found older hover directory layout, hover is now expecting a 'go' directory instead of 'desktop'.") 69 | log.Warnf("⚠ To migrate, rename the 'desktop' directory to 'go'.") 70 | log.Warnf(" Let hover do the migration? ") 71 | 72 | if askForConfirmation() { 73 | err := os.Rename(oldBuildPath, build.BuildPath) 74 | if err != nil { 75 | log.Warnf("Migration failed: %v", err) 76 | return false 77 | } 78 | log.Infof("Migration success") 79 | return true 80 | } 81 | 82 | return false 83 | } 84 | 85 | // askForConfirmation asks the user for confirmation. 86 | func askForConfirmation() bool { 87 | fmt.Print(log.Au().Bold(log.Au().Cyan("hover: ")).String() + "[y/N]? ") 88 | 89 | if len(os.Getenv("HOVER_DISABLE_INTERACTIONS")) > 0 { 90 | fmt.Println(log.Au().Bold(log.Au().Yellow("Interactions disabled, assuming 'no'.")).String()) 91 | return false 92 | } 93 | 94 | in := bufio.NewReader(os.Stdin) 95 | s, err := in.ReadString('\n') 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | s = strings.TrimSpace(s) 101 | s = strings.ToLower(s) 102 | 103 | if s == "y" || s == "yes" { 104 | return true 105 | } 106 | return false 107 | } 108 | 109 | var camelcaseRegex = regexp.MustCompile("(^[A-Za-z])|_([A-Za-z])") 110 | 111 | // toCamelCase take a snake_case string and converts it to camelcase 112 | func toCamelCase(str string) string { 113 | return camelcaseRegex.ReplaceAllStringFunc(str, func(s string) string { 114 | return strings.ToUpper(strings.Replace(s, "_", "", -1)) 115 | }) 116 | } 117 | 118 | // initializeGoModule uses the golang binary to initialize the go module 119 | func initializeGoModule(projectPath string) { 120 | wd, err := os.Getwd() 121 | if err != nil { 122 | log.Errorf("Failed to get working dir: %v\n", err) 123 | os.Exit(1) 124 | } 125 | 126 | cmdGoModInit := exec.Command(build.GoBin(), "mod", "init", projectPath+"/"+build.BuildPath) 127 | cmdGoModInit.Dir = filepath.Join(wd, build.BuildPath) 128 | cmdGoModInit.Env = append(os.Environ(), 129 | "GO111MODULE=on", 130 | ) 131 | cmdGoModInit.Stderr = os.Stderr 132 | cmdGoModInit.Stdout = os.Stdout 133 | err = cmdGoModInit.Run() 134 | if err != nil { 135 | log.Errorf("Go mod init failed: %v\n", err) 136 | os.Exit(1) 137 | } 138 | 139 | cmdGoModTidy := exec.Command(build.GoBin(), "mod", "tidy") 140 | cmdGoModTidy.Dir = filepath.Join(wd, build.BuildPath) 141 | log.Infof("You can add the '%s' directory to git.", cmdGoModTidy.Dir) 142 | cmdGoModTidy.Env = append(os.Environ(), 143 | "GO111MODULE=on", 144 | ) 145 | cmdGoModTidy.Stderr = os.Stderr 146 | cmdGoModTidy.Stdout = os.Stdout 147 | err = cmdGoModTidy.Run() 148 | if err != nil { 149 | log.Errorf("Go mod tidy failed: %v\n", err) 150 | os.Exit(1) 151 | } 152 | } 153 | 154 | // findPubcachePath returns the absolute path for the pub-cache or an error. 155 | func findPubcachePath() (string, error) { 156 | var path string 157 | switch runtime.GOOS { 158 | case "darwin", "linux": 159 | home, err := os.UserHomeDir() 160 | if err != nil { 161 | return "", errors.Wrap(err, "failed to resolve user home dir") 162 | } 163 | path = filepath.Join(home, ".pub-cache") 164 | case "windows": 165 | path = filepath.Join(os.Getenv("APPDATA"), "Pub", "Cache") 166 | } 167 | return path, nil 168 | } 169 | 170 | // shouldRunPluginGet checks if the pubspec.yaml file is older than the 171 | // .packages file, if it is the case, prompt the user for a hover plugin get. 172 | func shouldRunPluginGet() (bool, error) { 173 | file1Info, err := os.Stat("pubspec.yaml") 174 | if err != nil { 175 | return false, err 176 | } 177 | 178 | file2Info, err := os.Stat(".packages") 179 | if err != nil { 180 | if os.IsNotExist(err) { 181 | return true, nil 182 | } 183 | return false, err 184 | } 185 | modTime1 := file1Info.ModTime() 186 | modTime2 := file2Info.ModTime() 187 | 188 | diff := modTime1.Sub(modTime2) 189 | 190 | if diff > (time.Duration(0) * time.Second) { 191 | return true, nil 192 | } 193 | return false, nil 194 | } 195 | -------------------------------------------------------------------------------- /cmd/docker.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "os/user" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/go-flutter-desktop/hover/cmd/packaging" 13 | "github.com/go-flutter-desktop/hover/internal/build" 14 | "github.com/go-flutter-desktop/hover/internal/log" 15 | "github.com/go-flutter-desktop/hover/internal/logstreamer" 16 | "github.com/go-flutter-desktop/hover/internal/version" 17 | ) 18 | 19 | func dockerHoverBuild(targetOS string, packagingTask packaging.Task, buildFlags []string, vmArguments []string) { 20 | var err error 21 | dockerBin := build.DockerBin() 22 | 23 | hoverCacheDir := filepath.Join(buildOrRunCachePath, "hover") 24 | 25 | engineCacheDir := filepath.Join(hoverCacheDir, "engine") 26 | err = os.MkdirAll(engineCacheDir, 0755) 27 | if err != nil { 28 | log.Errorf("Cannot create the engine cache path in the user cache directory: %v", err) 29 | os.Exit(1) 30 | } 31 | 32 | dockerGoCacheDir := filepath.Join(hoverCacheDir, "docker-go-cache") 33 | err = os.MkdirAll(dockerGoCacheDir, 0755) 34 | if err != nil { 35 | log.Errorf("Cannot create the docker-go-cache path in the user cache directory: %v", err) 36 | os.Exit(1) 37 | } 38 | 39 | wd, err := os.Getwd() 40 | if err != nil { 41 | log.Errorf("Cannot get the path for current directory %s", err) 42 | os.Exit(1) 43 | } 44 | log.Infof("Building using docker container") 45 | 46 | dockerArgs := []string{ 47 | "run", 48 | "--rm", 49 | "--mount", "type=bind,source=" + wd + ",target=/app", 50 | "--mount", "type=bind,source=" + engineCacheDir + ",target=/root/.cache/hover/engine", 51 | "--mount", "type=bind,source=" + dockerGoCacheDir + ",target=/go-cache", 52 | "--env", "GOCACHE=/go-cache", 53 | } 54 | if runtime.GOOS != "windows" { 55 | currentUser, err := user.Current() 56 | if err != nil { 57 | log.Errorf("Couldn't get current user info: %v", err) 58 | os.Exit(1) 59 | } 60 | dockerArgs = append(dockerArgs, "--env", "HOVER_SAFE_CHOWN_UID="+currentUser.Uid) 61 | dockerArgs = append(dockerArgs, "--env", "HOVER_SAFE_CHOWN_GID="+currentUser.Gid) 62 | } 63 | goproxy, err := exec.Command("go", "env", "GOPROXY").Output() 64 | if err != nil { 65 | log.Errorf("Failed to get GOPROXY: %v", err) 66 | } 67 | if string(goproxy) != "" { 68 | dockerArgs = append(dockerArgs, "--env", "GOPROXY="+string(goproxy)) 69 | } 70 | goprivate, err := exec.Command("go", "env", "GOPRIVATE").Output() 71 | if err != nil { 72 | log.Errorf("Failed to get GOPRIVATE: %v", err) 73 | } 74 | if string(goprivate) != "" { 75 | dockerArgs = append(dockerArgs, "--env", "GOPRIVATE="+string(goprivate)) 76 | } 77 | if len(vmArguments) > 0 { 78 | // I (GeertJohan) am not too happy with this, it make the hover inside 79 | // the container aware of it being inside the container. But for now 80 | // this is the best way to go about. 81 | // 82 | // HOVER_BUILD_INDOCKER_VMARGS is explicitly not document, it is not 83 | // intended to be abused and may disappear at any time. 84 | dockerArgs = append(dockerArgs, "--env", "HOVER_IN_DOCKER_BUILD_VMARGS="+strings.Join(vmArguments, ",")) 85 | } 86 | 87 | hoverVersion := version.HoverVersion() 88 | if hoverVersion == "(devel)" { 89 | hoverVersion = "latest" 90 | } 91 | dockerImage := "goflutter/hover:" + hoverVersion 92 | dockerArgs = append(dockerArgs, dockerImage) 93 | targetOSAndPackaging := targetOS 94 | if packName := packagingTask.Name(); packName != "" { 95 | targetOSAndPackaging = packName 96 | } 97 | hoverCommand := []string{"hover-safe.sh", "build", targetOSAndPackaging} 98 | hoverCommand = append(hoverCommand, buildFlags...) 99 | dockerArgs = append(dockerArgs, hoverCommand...) 100 | 101 | dockerRunCmd := exec.Command(dockerBin, dockerArgs...) 102 | // TODO: remove debug line 103 | fmt.Printf("Running this docker command: %v\n", dockerRunCmd.String()) 104 | dockerRunCmd.Stderr = logstreamer.NewLogstreamerForStderr("docker container: ") 105 | dockerRunCmd.Stdout = logstreamer.NewLogstreamerForStdout("docker container: ") 106 | dockerRunCmd.Dir = wd 107 | err = dockerRunCmd.Run() 108 | if err != nil { 109 | log.Errorf("Docker run failed: %v", err) 110 | os.Exit(1) 111 | } 112 | log.Infof("Docker run completed") 113 | } 114 | -------------------------------------------------------------------------------- /cmd/doctor.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | "gopkg.in/yaml.v2" 15 | 16 | "github.com/go-flutter-desktop/hover/cmd/packaging" 17 | "github.com/go-flutter-desktop/hover/internal/build" 18 | "github.com/go-flutter-desktop/hover/internal/config" 19 | "github.com/go-flutter-desktop/hover/internal/log" 20 | "github.com/go-flutter-desktop/hover/internal/version" 21 | ) 22 | 23 | func init() { 24 | rootCmd.AddCommand(doctorCmd) 25 | } 26 | 27 | var doctorCmd = &cobra.Command{ 28 | Use: "doctor", 29 | Short: "Show information about the installed tooling", 30 | Args: func(cmd *cobra.Command, args []string) error { 31 | if len(args) > 0 { 32 | return errors.New("does not take arguments") 33 | } 34 | return nil 35 | }, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | assertInFlutterProject() 38 | 39 | hoverVersion := version.HoverVersion() 40 | log.Infof("Hover version %s running on %s", hoverVersion, runtime.GOOS) 41 | 42 | log.Infof("Sharing packaging tools") 43 | for _, task := range []packaging.Task{ 44 | packaging.DarwinBundleTask, 45 | packaging.DarwinDmgTask, 46 | packaging.DarwinPkgTask, 47 | packaging.LinuxAppImageTask, 48 | packaging.LinuxDebTask, 49 | packaging.LinuxPkgTask, 50 | packaging.LinuxRpmTask, 51 | packaging.LinuxSnapTask, 52 | packaging.WindowsMsiTask, 53 | } { 54 | if task.IsSupported() { 55 | log.Infof("%s is supported", task.Name()) 56 | } 57 | } 58 | log.Printf("") 59 | 60 | log.Infof("Sharing flutter version") 61 | cmdFlutterVersion := exec.Command(build.FlutterBin(), "--version") 62 | cmdFlutterVersion.Stderr = os.Stderr 63 | cmdFlutterVersion.Stdout = os.Stdout 64 | err := cmdFlutterVersion.Run() 65 | if err != nil { 66 | log.Errorf("Flutter --version failed: %v", err) 67 | } 68 | 69 | engineCommitHash := version.FlutterRequiredEngineVersion() 70 | log.Infof("Flutter engine commit: %s", log.Au().Magenta("https://github.com/flutter/engine/commit/"+engineCommitHash)) 71 | 72 | checkFlutterChannel() 73 | 74 | cmdGoEnvCC := exec.Command(build.GoBin(), "env", "CC") 75 | cmdGoEnvCCOut, err := cmdGoEnvCC.Output() 76 | if err != nil { 77 | log.Errorf("Go env CC failed: %v", err) 78 | } 79 | cCompiler := strings.Trim(string(cmdGoEnvCCOut), " ") 80 | cCompiler = strings.Trim(cCompiler, "\n") 81 | if cCompiler != "" { 82 | log.Infof("Finding out the C compiler version") 83 | cmdCCVersion := exec.Command(cCompiler, "--version") 84 | cmdCCVersion.Stderr = os.Stderr 85 | cmdCCVersion.Stdout = os.Stdout 86 | cmdCCVersion.Run() 87 | } 88 | 89 | log.Infof("Sharing the content of go.mod") 90 | file, err := os.Open(filepath.Join(build.BuildPath, "go.mod")) 91 | if err != nil { 92 | log.Errorf("Failed to read go.mod: %v", err) 93 | } else { 94 | defer file.Close() 95 | b, _ := ioutil.ReadAll(file) 96 | fmt.Print(string(b)) 97 | } 98 | 99 | hoverConfig, err := config.ReadConfigFile(filepath.Join(build.BuildPath, "hover.yaml")) 100 | if err != nil { 101 | log.Warnf("%v", err) 102 | } else { 103 | log.Infof("Sharing the content of hover.yaml") 104 | dump, err := yaml.Marshal(hoverConfig) 105 | if err != nil { 106 | log.Warnf("%v", err) 107 | } else { 108 | fmt.Print(string(dump)) 109 | } 110 | } 111 | 112 | log.Infof("Sharing the content of go/cmd") 113 | files, err := filepath.Glob(filepath.Join(build.BuildPath, "cmd", "*")) 114 | if err != nil { 115 | log.Errorf("Failed to get the list of files in go/cmd", err) 116 | os.Exit(1) 117 | } 118 | fmt.Println(strings.Join(files, "\t")) 119 | }, 120 | } 121 | -------------------------------------------------------------------------------- /cmd/init-plugin.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/go-flutter-desktop/hover/internal/build" 11 | "github.com/go-flutter-desktop/hover/internal/fileutils" 12 | "github.com/go-flutter-desktop/hover/internal/log" 13 | "github.com/go-flutter-desktop/hover/internal/pubspec" 14 | ) 15 | 16 | func init() { 17 | rootCmd.AddCommand(createPluginCmd) 18 | } 19 | 20 | var createPluginCmd = &cobra.Command{ 21 | Use: "init-plugin", 22 | Short: "Initialize a go-flutter plugin in a existing flutter platform plugin", 23 | Args: func(cmd *cobra.Command, args []string) error { 24 | if len(args) != 1 { 25 | return errors.New("requires one argument, the VCS repository path. e.g.: github.com/my-organization/" + pubspec.GetPubSpec().Name + "\n" + 26 | "This path will be used by Golang to fetch the plugin, make sure it correspond to the code repository of the plugin!") 27 | } 28 | return nil 29 | }, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | assertInFlutterPluginProject() 32 | 33 | vcsPath := args[0] 34 | 35 | err := os.Mkdir(build.BuildPath, 0775) 36 | if err != nil { 37 | if os.IsExist(err) { 38 | log.Errorf("A file or directory named `" + build.BuildPath + "` already exists. Cannot continue init-plugin.") 39 | os.Exit(1) 40 | } 41 | log.Errorf("Failed to create '%s' directory: %v", build.BuildPath, err) 42 | os.Exit(1) 43 | } 44 | 45 | templateData := map[string]string{ 46 | "pluginName": pubspec.GetPubSpec().Name, 47 | "structName": toCamelCase(pubspec.GetPubSpec().Name + "Plugin"), 48 | "urlVSCRepo": vcsPath, 49 | } 50 | 51 | fileutils.ExecuteTemplateFromAssets("plugin/plugin.go.tmpl", filepath.Join(build.BuildPath, "plugin.go"), templateData) 52 | fileutils.ExecuteTemplateFromAssets("plugin/README.md.tmpl", filepath.Join(build.BuildPath, "README.md"), templateData) 53 | fileutils.ExecuteTemplateFromAssets("plugin/import.go.tmpl.tmpl", filepath.Join(build.BuildPath, "import.go.tmpl"), templateData) 54 | 55 | dlibPath := filepath.Join(build.BuildPath, "dlib") 56 | err = os.Mkdir(dlibPath, 0775) 57 | if err != nil { 58 | log.Errorf("Failed to create '%s' directory: %v", dlibPath, err) 59 | os.Exit(1) 60 | } 61 | fileutils.ExecuteTemplateFromAssets("plugin/README.md.dlib.tmpl", filepath.Join(dlibPath, "README.md"), templateData) 62 | 63 | platforms := []string{"darwin", "linux", "windows"} 64 | for _, platform := range platforms { 65 | platformPath := filepath.Join(dlibPath, platform) 66 | err = os.Mkdir(platformPath, 0775) 67 | if err != nil { 68 | log.Errorf("Failed to create '%s' directory: %v", platformPath, err) 69 | os.Exit(1) 70 | } 71 | } 72 | initializeGoModule(vcsPath) 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/go-flutter-desktop/hover/internal/build" 11 | "github.com/go-flutter-desktop/hover/internal/config" 12 | "github.com/go-flutter-desktop/hover/internal/fileutils" 13 | "github.com/go-flutter-desktop/hover/internal/log" 14 | "github.com/go-flutter-desktop/hover/internal/pubspec" 15 | ) 16 | 17 | func init() { 18 | rootCmd.AddCommand(initCmd) 19 | } 20 | 21 | var initCmd = &cobra.Command{ 22 | Use: "init [project]", 23 | Short: "Initialize a flutter project to use go-flutter", 24 | Args: func(cmd *cobra.Command, args []string) error { 25 | if len(args) > 1 { 26 | return errors.New("allows only one argument, the project path. e.g.: github.com/my-organization/my-app") 27 | } 28 | return nil 29 | }, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | assertInFlutterProject() 32 | 33 | projectName := pubspec.GetPubSpec().Name 34 | 35 | var projectPath string 36 | if len(args) == 0 || args[0] == "." { 37 | projectPath = projectName 38 | } else { 39 | projectPath = args[0] 40 | } 41 | 42 | err := os.Mkdir(build.BuildPath, 0775) 43 | if err != nil { 44 | if os.IsExist(err) { 45 | log.Errorf("A file or directory named '%s' already exists. Cannot continue init.", build.BuildPath) 46 | os.Exit(1) 47 | } 48 | log.Errorf("Failed to create '%s' directory: %v", build.BuildPath, err) 49 | os.Exit(1) 50 | } 51 | 52 | desktopCmdPath := filepath.Join(build.BuildPath, "cmd") 53 | err = os.Mkdir(desktopCmdPath, 0775) 54 | if err != nil { 55 | log.Errorf("Failed to create '%s': %v", desktopCmdPath, err) 56 | os.Exit(1) 57 | } 58 | 59 | desktopAssetsPath := filepath.Join(build.BuildPath, "assets") 60 | err = os.Mkdir(desktopAssetsPath, 0775) 61 | if err != nil { 62 | log.Errorf("Failed to create '%s': %v", desktopAssetsPath, err) 63 | os.Exit(1) 64 | } 65 | 66 | emptyConfig := config.Config{} 67 | 68 | fileutils.CopyAsset("app/main.go.tmpl", filepath.Join(desktopCmdPath, "main.go")) 69 | fileutils.CopyAsset("app/options.go.tmpl", filepath.Join(desktopCmdPath, "options.go")) 70 | fileutils.CopyAsset("app/icon.png", filepath.Join(desktopAssetsPath, "icon.png")) 71 | fileutils.CopyAsset("app/gitignore", filepath.Join(build.BuildPath, ".gitignore")) 72 | fileutils.ExecuteTemplateFromAssets("app/hover.yaml.tmpl", filepath.Join(build.BuildPath, "hover.yaml"), map[string]string{ 73 | "applicationName": emptyConfig.GetApplicationName(projectName), 74 | "executableName": emptyConfig.GetExecutableName(projectName), 75 | "packageName": emptyConfig.GetPackageName(projectName), 76 | }) 77 | 78 | initializeGoModule(projectPath) 79 | log.Printf("Available plugin for this project:") 80 | pluginListCmd.Run(cmd, []string{}) 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /cmd/packaging.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/go-flutter-desktop/hover/cmd/packaging" 7 | ) 8 | 9 | func init() { 10 | initPackagingCmd.AddCommand(initLinuxSnapCmd) 11 | initPackagingCmd.AddCommand(initLinuxDebCmd) 12 | initPackagingCmd.AddCommand(initLinuxAppImageCmd) 13 | initPackagingCmd.AddCommand(initLinuxRpmCmd) 14 | initPackagingCmd.AddCommand(initLinuxPkgCmd) 15 | initPackagingCmd.AddCommand(initWindowsMsiCmd) 16 | initPackagingCmd.AddCommand(initDarwinBundleCmd) 17 | initPackagingCmd.AddCommand(initDarwinPkgCmd) 18 | initPackagingCmd.AddCommand(initDarwinDmgCmd) 19 | rootCmd.AddCommand(initPackagingCmd) 20 | } 21 | 22 | var initPackagingCmd = &cobra.Command{ 23 | Use: "init-packaging", 24 | Short: "Create configuration files for a packaging format", 25 | } 26 | 27 | var initLinuxSnapCmd = &cobra.Command{ 28 | Use: "linux-snap", 29 | Short: "Create configuration files for snap packaging", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | assertHoverInitialized() 32 | 33 | packaging.LinuxSnapTask.Init() 34 | }, 35 | } 36 | 37 | var initLinuxDebCmd = &cobra.Command{ 38 | Use: "linux-deb", 39 | Short: "Create configuration files for deb packaging", 40 | Run: func(cmd *cobra.Command, args []string) { 41 | assertHoverInitialized() 42 | 43 | packaging.LinuxDebTask.Init() 44 | }, 45 | } 46 | 47 | var initLinuxAppImageCmd = &cobra.Command{ 48 | Use: "linux-appimage", 49 | Short: "Create configuration files for AppImage packaging", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | assertHoverInitialized() 52 | 53 | packaging.LinuxAppImageTask.Init() 54 | }, 55 | } 56 | var initLinuxRpmCmd = &cobra.Command{ 57 | Use: "linux-rpm", 58 | Short: "Create configuration files for rpm packaging", 59 | Run: func(cmd *cobra.Command, args []string) { 60 | assertHoverInitialized() 61 | 62 | packaging.LinuxRpmTask.Init() 63 | }, 64 | } 65 | var initLinuxPkgCmd = &cobra.Command{ 66 | Use: "linux-pkg", 67 | Short: "Create configuration files for pacman pkg packaging", 68 | Run: func(cmd *cobra.Command, args []string) { 69 | assertHoverInitialized() 70 | 71 | packaging.LinuxPkgTask.Init() 72 | }, 73 | } 74 | var initWindowsMsiCmd = &cobra.Command{ 75 | Use: "windows-msi", 76 | Short: "Create configuration files for msi packaging", 77 | Run: func(cmd *cobra.Command, args []string) { 78 | assertHoverInitialized() 79 | 80 | packaging.WindowsMsiTask.Init() 81 | }, 82 | } 83 | 84 | var initDarwinBundleCmd = &cobra.Command{ 85 | Use: "darwin-bundle", 86 | Short: "Create configuration files for OSX bundle packaging", 87 | Run: func(cmd *cobra.Command, args []string) { 88 | assertHoverInitialized() 89 | 90 | packaging.DarwinBundleTask.Init() 91 | }, 92 | } 93 | 94 | var initDarwinPkgCmd = &cobra.Command{ 95 | Use: "darwin-pkg", 96 | Short: "Create configuration files for OSX pkg installer packaging", 97 | Run: func(cmd *cobra.Command, args []string) { 98 | assertHoverInitialized() 99 | 100 | packaging.DarwinPkgTask.Init() 101 | }, 102 | } 103 | 104 | var initDarwinDmgCmd = &cobra.Command{ 105 | Use: "darwin-dmg", 106 | Short: "Create configuration files for OSX dmg packaging", 107 | Run: func(cmd *cobra.Command, args []string) { 108 | assertHoverInitialized() 109 | 110 | packaging.DarwinDmgTask.Init() 111 | }, 112 | } 113 | -------------------------------------------------------------------------------- /cmd/packaging/darwin-bundle.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/JackMordaunt/icns" 10 | ) 11 | 12 | // DarwinBundleTask packaging for darwin as bundle 13 | var DarwinBundleTask = &packagingTask{ 14 | packagingFormatName: "darwin-bundle", 15 | templateFiles: map[string]string{ 16 | "darwin-bundle/Info.plist.tmpl": "{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl", 17 | }, 18 | executableFiles: []string{}, 19 | flutterBuildOutputDirectory: "{{.applicationName}} {{.version}}.app/Contents/MacOS", 20 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 21 | outputFileName := fmt.Sprintf("%s %s.app", applicationName, version) 22 | err := os.MkdirAll(filepath.Join(tmpPath, outputFileName, "Contents", "Resources"), 0755) 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | pngFile, err := os.Open(filepath.Join(tmpPath, outputFileName, "Contents", "MacOS", "assets", "icon.png")) 28 | if err != nil { 29 | return "", err 30 | } 31 | defer pngFile.Close() 32 | 33 | srcImg, _, err := image.Decode(pngFile) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | icnsFile, err := os.Create(filepath.Join(tmpPath, outputFileName, "Contents", "Resources", "icon.icns")) 39 | if err != nil { 40 | return "", err 41 | } 42 | defer icnsFile.Close() 43 | 44 | err = icns.Encode(icnsFile, srcImg) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | return outputFileName, nil 50 | }, 51 | requiredTools: map[string]map[string]string{ 52 | "linux": {}, 53 | "darwin": {}, 54 | "windows": {}, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /cmd/packaging/darwin-dmg.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | ) 10 | 11 | // DarwinDmgTask packaging for darwin as dmg 12 | var DarwinDmgTask = &packagingTask{ 13 | packagingFormatName: "darwin-dmg", 14 | dependsOn: map[*packagingTask]string{ 15 | DarwinBundleTask: "dmgdir", 16 | }, 17 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 18 | outputFileName := fmt.Sprintf("%s %s.dmg", applicationName, version) 19 | cmdLn := exec.Command("ln", "-sf", "/Applications", "dmgdir/Applications") 20 | cmdLn.Dir = tmpPath 21 | cmdLn.Stdout = os.Stdout 22 | cmdLn.Stderr = os.Stderr 23 | err := cmdLn.Run() 24 | if err != nil { 25 | return "", err 26 | } 27 | appBundleOriginalPath := filepath.Join(tmpPath, "dmgdir", fmt.Sprintf("%s %s.app", applicationName, version)) 28 | appBundleFinalPath := filepath.Join(tmpPath, "dmgdir", fmt.Sprintf("%s.app", applicationName)) 29 | err = os.Rename(appBundleOriginalPath, appBundleFinalPath) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | var cmdCreateBundle *exec.Cmd 35 | switch os := runtime.GOOS; os { 36 | case "darwin": 37 | cmdCreateBundle = exec.Command("hdiutil", "create", "-volname", packageName, "-srcfolder", "dmgdir", "-ov", "-format", "UDBZ", outputFileName) 38 | case "linux": 39 | cmdCreateBundle = exec.Command("mkisofs", "-V", packageName, "-D", "-R", "-apple", "-no-pad", "-o", outputFileName, "dmgdir") 40 | } 41 | cmdCreateBundle.Dir = tmpPath 42 | cmdCreateBundle.Stdout = os.Stdout 43 | cmdCreateBundle.Stderr = os.Stderr 44 | err = cmdCreateBundle.Run() 45 | if err != nil { 46 | return "", err 47 | } 48 | return outputFileName, nil 49 | }, 50 | skipAssertInitialized: true, 51 | requiredTools: map[string]map[string]string{ 52 | "linux": { 53 | "ln": "Install ln from your package manager", 54 | "mkisofs": "Install mkisofs from your package manager. Some distros ship genisoimage which is a fork of mkisofs. Create a symlink for it like this: ln -s $(which genisoimage) /usr/bin/mkisofs", 55 | }, 56 | "darwin": { 57 | "ln": "Install ln from your package manager", 58 | "hdiutil": "Install hdiutil from your package manager", 59 | }, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /cmd/packaging/darwin-pkg.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // DarwinPkgTask packaging for darwin as pkg 14 | var DarwinPkgTask = &packagingTask{ 15 | packagingFormatName: "darwin-pkg", 16 | dependsOn: map[*packagingTask]string{ 17 | DarwinBundleTask: "flat/root/Applications", 18 | }, 19 | templateFiles: map[string]string{ 20 | "darwin-pkg/PackageInfo.tmpl": "flat/base.pkg/PackageInfo.tmpl", 21 | "darwin-pkg/Distribution.tmpl": "flat/Distribution.tmpl", 22 | }, 23 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 24 | outputFileName := fmt.Sprintf("%s %s.pkg", applicationName, version) 25 | 26 | payload, err := os.OpenFile(filepath.Join(tmpPath, "flat", "base.pkg", "Payload"), os.O_RDWR|os.O_CREATE, 0755) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | appBundleOriginalPath := filepath.Join(tmpPath, "flat", "root", "Applications", fmt.Sprintf("%s %s.app", applicationName, version)) 32 | appBundleFinalPath := filepath.Join(tmpPath, "flat", "root", "Applications", fmt.Sprintf("%s.app", applicationName)) 33 | err = os.Rename(appBundleOriginalPath, appBundleFinalPath) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | cmdFind := exec.Command("find", ".") 39 | cmdFind.Dir = filepath.Join(tmpPath, "flat", "root") 40 | cmdCpio := exec.Command("cpio", "-o", "--format", "odc", "--owner", "0:80") 41 | cmdCpio.Dir = filepath.Join(tmpPath, "flat", "root") 42 | cmdGzip := exec.Command("gzip", "-c") 43 | 44 | // Pipes like this: find | cpio | gzip > Payload 45 | cmdCpio.Stdin, err = cmdFind.StderrPipe() 46 | if err != nil { 47 | return "", err 48 | } 49 | cmdGzip.Stdin, err = cmdCpio.StderrPipe() 50 | if err != nil { 51 | return "", err 52 | } 53 | cmdGzip.Stdout = payload 54 | 55 | err = cmdGzip.Start() 56 | if err != nil { 57 | return "", err 58 | } 59 | err = cmdCpio.Start() 60 | if err != nil { 61 | return "", err 62 | } 63 | err = cmdFind.Run() 64 | if err != nil { 65 | return "", err 66 | } 67 | err = cmdCpio.Wait() 68 | if err != nil { 69 | return "", err 70 | } 71 | err = cmdGzip.Wait() 72 | if err != nil { 73 | return "", err 74 | } 75 | err = payload.Close() 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | var cmdMkbom *exec.Cmd 81 | switch os := runtime.GOOS; os { 82 | case "darwin": 83 | cmdMkbom = exec.Command("mkbom", filepath.Join("flat", "root"), filepath.Join("flat", "base.pkg", "Payload")) 84 | case "linux": 85 | cmdMkbom = exec.Command("mkbom", "-u", "0", "-g", "80", filepath.Join("flat", "root"), filepath.Join("flat", "base.pkg", "Payload")) 86 | } 87 | cmdMkbom.Dir = tmpPath 88 | cmdMkbom.Stdout = os.Stdout 89 | cmdMkbom.Stderr = os.Stderr 90 | err = cmdMkbom.Run() 91 | if err != nil { 92 | return "", nil 93 | } 94 | 95 | var files []string 96 | err = filepath.Walk(filepath.Join(tmpPath, "flat"), func(path string, info os.FileInfo, err error) error { 97 | relativePath, err := filepath.Rel(filepath.Join(tmpPath, "flat"), path) 98 | if err != nil { 99 | return err 100 | } 101 | files = append(files, relativePath) 102 | return nil 103 | }) 104 | if err != nil { 105 | return "", errors.Wrap(err, "failed to iterate over ") 106 | } 107 | 108 | cmdXar := exec.Command("xar", append([]string{"--compression", "none", "-cf", filepath.Join("..", outputFileName)}, files...)...) 109 | cmdXar.Dir = filepath.Join(tmpPath, "flat") 110 | cmdXar.Stdout = os.Stdout 111 | cmdXar.Stderr = os.Stderr 112 | err = cmdXar.Run() 113 | if err != nil { 114 | return "", errors.Wrap(err, "failed to run xar") 115 | } 116 | return outputFileName, nil 117 | }, 118 | requiredTools: map[string]map[string]string{ 119 | "linux": { 120 | "find": "Install find from your package manager", 121 | "cpio": "Install cpio from your package manager", 122 | "gzip": "Install gzip from your package manager", 123 | "mkbom": "Install bomutils from your package manager or from https://github.com/hogliux/bomutils", 124 | "xar": "Install xar from your package manager or from https://github.com/mackyle/xar", 125 | }, 126 | "darwin": { 127 | "find": "Install find from your package manager", 128 | "cpio": "Install cpio from your package manager", 129 | "gzip": "Install gzip from your package manager", 130 | "mkbom": "Install bomutils from your package manager or from https://github.com/hogliux/bomutils", 131 | "xar": "Install xar from your package manager or from https://github.com/mackyle/xar", 132 | }, 133 | }, 134 | } 135 | -------------------------------------------------------------------------------- /cmd/packaging/linux-appimage.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | copy "github.com/otiai10/copy" 11 | 12 | "github.com/go-flutter-desktop/hover/internal/log" 13 | ) 14 | 15 | // LinuxAppImageTask packaging for linux as AppImage 16 | var LinuxAppImageTask = &packagingTask{ 17 | packagingFormatName: "linux-appimage", 18 | templateFiles: map[string]string{ 19 | "linux-appimage/AppRun.tmpl": "AppRun.tmpl", 20 | "linux/app.desktop.tmpl": "{{.packageName}}.desktop.tmpl", 21 | }, 22 | executableFiles: []string{ 23 | ".", 24 | "AppRun", 25 | "{{.packageName}}.desktop", 26 | }, 27 | linuxDesktopFileIconPath: "{{.packageName}}", 28 | flutterBuildOutputDirectory: "build", 29 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 30 | sourceIconPath := filepath.Join(tmpPath, "build", "assets", "icon.png") 31 | iconDir := filepath.Join(tmpPath, "usr", "share", "icons", "hicolor", "256x256", "apps") 32 | if _, err := os.Stat(iconDir); os.IsNotExist(err) { 33 | err = os.MkdirAll(iconDir, 0755) 34 | if err != nil { 35 | log.Errorf("Failed to create icon dir: %v", err) 36 | os.Exit(1) 37 | } 38 | } 39 | err := copy.Copy(sourceIconPath, filepath.Join(tmpPath, fmt.Sprintf("%s.png", packageName))) 40 | if err != nil { 41 | log.Errorf("Failed to copy icon root dir: %v", err) 42 | os.Exit(1) 43 | } 44 | err = copy.Copy(sourceIconPath, filepath.Join(iconDir, fmt.Sprintf("%s.png", packageName))) 45 | if err != nil { 46 | log.Errorf("Failed to copy icon dir: %v", err) 47 | os.Exit(1) 48 | } 49 | cmdAppImageTool := exec.Command("appimagetool", ".") 50 | cmdAppImageTool.Dir = tmpPath 51 | cmdAppImageTool.Stdout = os.Stdout 52 | cmdAppImageTool.Stderr = os.Stderr 53 | cmdAppImageTool.Env = append( 54 | os.Environ(), 55 | "ARCH=x86_64", 56 | fmt.Sprintf("VERSION=%s", version), 57 | ) 58 | err = cmdAppImageTool.Run() 59 | if err != nil { 60 | return "", err 61 | } 62 | return fmt.Sprintf("%s-%s-x86_64.AppImage", strings.ReplaceAll(applicationName, " ", "_"), version), nil 63 | }, 64 | requiredTools: map[string]map[string]string{ 65 | "linux": { 66 | "appimagetool": "Install appimagetool from your package manager or from https://github.com/AppImage/AppImageKit#appimagetool-usage", 67 | }, 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /cmd/packaging/linux-deb.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // LinuxDebTask packaging for linux as deb 10 | var LinuxDebTask = &packagingTask{ 11 | packagingFormatName: "linux-deb", 12 | templateFiles: map[string]string{ 13 | "linux-deb/control.tmpl": "DEBIAN/control.tmpl", 14 | "linux/bin.tmpl": "usr/bin/{{.executableName}}.tmpl", 15 | "linux/app.desktop.tmpl": "usr/share/applications/{{.executableName}}.desktop.tmpl", 16 | }, 17 | executableFiles: []string{ 18 | "usr/bin/{{.executableName}}", 19 | "usr/share/applications/{{.executableName}}.desktop", 20 | }, 21 | linuxDesktopFileExecutablePath: "/usr/lib/{{.packageName}}/{{.executableName}}", 22 | linuxDesktopFileIconPath: "/usr/lib/{{.packageName}}/assets/icon.png", 23 | flutterBuildOutputDirectory: "usr/lib/{{.packageName}}", 24 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 25 | outputFileName := fmt.Sprintf("%s_%s_amd64.deb", packageName, version) 26 | cmdDpkgDeb := exec.Command("dpkg-deb", "--build", ".", outputFileName) 27 | cmdDpkgDeb.Dir = tmpPath 28 | cmdDpkgDeb.Stdout = os.Stdout 29 | cmdDpkgDeb.Stderr = os.Stderr 30 | err := cmdDpkgDeb.Run() 31 | if err != nil { 32 | return "", err 33 | } 34 | return outputFileName, nil 35 | }, 36 | requiredTools: map[string]map[string]string{ 37 | "linux": { 38 | "dpkg-deb": "You need to be on Debian, Ubuntu or another distro that uses apt/dpkg as package manager to use this. Installing dpkg on other distros is hard and dangerous.", 39 | }, 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /cmd/packaging/linux-pkg.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // LinuxPkgTask packaging for linux as pacman pkg 10 | var LinuxPkgTask = &packagingTask{ 11 | packagingFormatName: "linux-pkg", 12 | templateFiles: map[string]string{ 13 | "linux-pkg/PKGBUILD.tmpl": "PKGBUILD.tmpl", 14 | "linux/bin.tmpl": "src/usr/bin/{{.executableName}}.tmpl", 15 | "linux/app.desktop.tmpl": "src/usr/share/applications/{{.executableName}}.desktop.tmpl", 16 | }, 17 | executableFiles: []string{ 18 | "src/usr/bin/{{.executableName}}", 19 | "src/usr/share/applications/{{.executableName}}.desktop", 20 | }, 21 | linuxDesktopFileExecutablePath: "/usr/lib/{{.packageName}}/{{.executableName}}", 22 | linuxDesktopFileIconPath: "/usr/lib/{{.packageName}}/assets/icon.png", 23 | flutterBuildOutputDirectory: "src/usr/lib/{{.packageName}}", 24 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 25 | extension := ".pkg.tar.xz" 26 | cmdMakepkg := exec.Command("makepkg") 27 | cmdMakepkg.Dir = tmpPath 28 | cmdMakepkg.Stdout = os.Stdout 29 | cmdMakepkg.Stderr = os.Stderr 30 | cmdMakepkg.Env = append(os.Environ(), fmt.Sprintf("PKGEXT=%s", extension)) 31 | err := cmdMakepkg.Run() 32 | if err != nil { 33 | return "", err 34 | } 35 | return fmt.Sprintf("%s-%s-%s-x86_64%s", packageName, version, release, extension), nil 36 | }, 37 | requiredTools: map[string]map[string]string{ 38 | "linux": { 39 | "makepkg": "You need to be on Arch Linux or another distro that uses pacman as package manager to use this. Installing makepkg on other distros is hard and dangerous.", 40 | }, 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /cmd/packaging/linux-rpm.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // LinuxRpmTask packaging for linux as rpm 10 | var LinuxRpmTask = &packagingTask{ 11 | packagingFormatName: "linux-rpm", 12 | templateFiles: map[string]string{ 13 | "linux-rpm/app.spec.tmpl": "SPECS/{{.packageName}}.spec.tmpl", 14 | "linux/bin.tmpl": "BUILDROOT/{{.packageName}}-{{.version}}-{{.release}}.x86_64/usr/bin/{{.executableName}}.tmpl", 15 | "linux/app.desktop.tmpl": "BUILDROOT/{{.packageName}}-{{.version}}-{{.release}}.x86_64/usr/share/applications/{{.executableName}}.desktop.tmpl", 16 | }, 17 | executableFiles: []string{ 18 | "BUILDROOT/{{.packageName}}-{{.version}}-{{.release}}.x86_64/usr/bin/{{.executableName}}", 19 | "BUILDROOT/{{.packageName}}-{{.version}}-{{.release}}.x86_64/usr/share/applications/{{.executableName}}.desktop", 20 | }, 21 | linuxDesktopFileExecutablePath: "/usr/lib/{{.packageName}}/{{.executableName}}", 22 | linuxDesktopFileIconPath: "/usr/lib/{{.packageName}}/assets/icon.png", 23 | flutterBuildOutputDirectory: "BUILD/{{.packageName}}-{{.version}}-{{.release}}.x86_64/usr/lib/{{.packageName}}", 24 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 25 | cmdRpmbuild := exec.Command("rpmbuild", "--define", fmt.Sprintf("_topdir %s", tmpPath), "--define", "_unpackaged_files_terminate_build 0", "-ba", fmt.Sprintf("./SPECS/%s.spec", packageName)) 26 | cmdRpmbuild.Dir = tmpPath 27 | cmdRpmbuild.Stdout = os.Stdout 28 | cmdRpmbuild.Stderr = os.Stderr 29 | err := cmdRpmbuild.Run() 30 | if err != nil { 31 | return "", err 32 | } 33 | return fmt.Sprintf("RPMS/x86_64/%s-%s-%s.x86_64.rpm", packageName, version, release), nil 34 | }, 35 | requiredTools: map[string]map[string]string{ 36 | "linux": { 37 | "rpmbuild": "You need to be on Red Hat Linux or another distro that uses rpm as package manager to use this. Installing rpmbuild on other distros is hard and dangerous.", 38 | }, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /cmd/packaging/linux-snap.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // LinuxSnapTask packaging for linux as snap 10 | var LinuxSnapTask = &packagingTask{ 11 | packagingFormatName: "linux-snap", 12 | templateFiles: map[string]string{ 13 | "linux-snap/snapcraft.yaml.tmpl": "snap/snapcraft.yaml.tmpl", 14 | "linux/app.desktop.tmpl": "snap/local/{{.executableName}}.desktop.tmpl", 15 | }, 16 | linuxDesktopFileExecutablePath: "/{{.executableName}}", 17 | linuxDesktopFileIconPath: "/icon.png", 18 | flutterBuildOutputDirectory: "build", 19 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 20 | cmdSnapcraft := exec.Command("snapcraft") 21 | cmdSnapcraft.Dir = tmpPath 22 | cmdSnapcraft.Stdout = os.Stdout 23 | cmdSnapcraft.Stderr = os.Stderr 24 | err := cmdSnapcraft.Run() 25 | if err != nil { 26 | return "", err 27 | } 28 | return fmt.Sprintf("%s_%s_amd64.snap", packageName, version), nil 29 | }, 30 | requiredTools: map[string]map[string]string{ 31 | "linux": { 32 | "snapcraft": "Install snapd from your package manager or from https://snapcraft.io/docs/installing-snapd", 33 | }, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /cmd/packaging/noop.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import "github.com/go-flutter-desktop/hover/internal/build" 4 | 5 | type noopTask struct{} 6 | 7 | var NoopTask Task = &noopTask{} 8 | 9 | func (_ *noopTask) Name() string { return "" } 10 | func (_ *noopTask) Init() {} 11 | func (_ *noopTask) IsInitialized() bool { return true } 12 | func (_ *noopTask) AssertInitialized() {} 13 | func (_ *noopTask) Pack(string, build.Mode) {} 14 | func (_ *noopTask) IsSupported() bool { return true } 15 | func (_ *noopTask) AssertSupported() {} 16 | -------------------------------------------------------------------------------- /cmd/packaging/packaging.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/go-flutter-desktop/hover/internal/pubspec" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "text/template" 14 | 15 | "github.com/otiai10/copy" 16 | 17 | "github.com/go-flutter-desktop/hover/internal/build" 18 | "github.com/go-flutter-desktop/hover/internal/config" 19 | "github.com/go-flutter-desktop/hover/internal/fileutils" 20 | "github.com/go-flutter-desktop/hover/internal/log" 21 | ) 22 | 23 | var packagingPath = filepath.Join(build.BuildPath, "packaging") 24 | 25 | func packagingFormatPath(packagingFormat string) string { 26 | directoryPath, err := filepath.Abs(filepath.Join(packagingPath, packagingFormat)) 27 | if err != nil { 28 | log.Errorf("Failed to resolve absolute path for %s directory: %v", packagingFormat, err) 29 | os.Exit(1) 30 | } 31 | return directoryPath 32 | } 33 | 34 | func createPackagingFormatDirectory(packagingFormat string) { 35 | if _, err := os.Stat(packagingFormatPath(packagingFormat)); !os.IsNotExist(err) { 36 | log.Errorf("A file or directory named `%s` already exists. Cannot continue packaging init for %s.", packagingFormat, packagingFormat) 37 | os.Exit(1) 38 | } 39 | err := os.MkdirAll(packagingFormatPath(packagingFormat), 0775) 40 | if err != nil { 41 | log.Errorf("Failed to create %s directory %s: %v", packagingFormat, packagingFormatPath(packagingFormat), err) 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func getTemporaryBuildDirectory(projectName string, packagingFormat string) string { 47 | tmpPath, err := ioutil.TempDir("", "hover-build-"+projectName+"-"+packagingFormat) 48 | if err != nil { 49 | log.Errorf("Couldn't get temporary build directory: %v", err) 50 | os.Exit(1) 51 | } 52 | return tmpPath 53 | } 54 | 55 | type packagingTask struct { 56 | packagingFormatName string // Name of the packaging format: OS-TYPE 57 | dependsOn map[*packagingTask]string // Packaging tasks this task depends on 58 | templateFiles map[string]string // Template files to copy over on init 59 | executableFiles []string // Files that should be executable 60 | linuxDesktopFileExecutablePath string // Path of the executable for linux .desktop file (only set on linux) 61 | linuxDesktopFileIconPath string // Path of the icon for linux .desktop file (only set on linux) 62 | generateBuildFiles func(packageName, path string) // Generate dynamic build files. Operates in the temporary directory 63 | generateInitFiles func(packageName, path string) // Generate dynamic init files 64 | extraTemplateData func(packageName, path string) map[string]string // Update the template data on build. This is used for inserting values that are generated on init 65 | flutterBuildOutputDirectory string // Path to copy the build output of the app to. Operates in the temporary directory 66 | packagingFunction func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) // Function that actually packages the app. Needs to check for OS specific tools etc. . Returns the path of the packaged file 67 | skipAssertInitialized bool // Set to true when a task doesn't need to be initialized. 68 | requiredTools map[string]map[string]string // Map of list of tools required to package per OS 69 | } 70 | 71 | func (t *packagingTask) AssertSupported() { 72 | if !t.IsSupported() { 73 | os.Exit(1) 74 | } 75 | } 76 | 77 | func (t *packagingTask) IsSupported() bool { 78 | for task := range t.dependsOn { 79 | if !task.IsSupported() { 80 | return false 81 | } 82 | } 83 | if _, osIsSupported := t.requiredTools[runtime.GOOS]; !osIsSupported { 84 | log.Errorf("Packaging %s is not supported on %s", t.packagingFormatName, runtime.GOOS) 85 | log.Errorf("To still package %s on %s you need to run hover with the `--docker` flag.", t.packagingFormatName, runtime.GOOS) 86 | return false 87 | } 88 | var unavailableTools []string 89 | for tool := range t.requiredTools[runtime.GOOS] { 90 | _, err := exec.LookPath(tool) 91 | if err != nil { 92 | unavailableTools = append(unavailableTools, tool) 93 | } 94 | } 95 | if len(unavailableTools) > 0 { 96 | log.Errorf("To package %s these tools are required: %s", t.packagingFormatName, strings.Join(unavailableTools, ",")) 97 | for _, tool := range unavailableTools { 98 | text := t.requiredTools[runtime.GOOS][tool] 99 | if len(text) > 0 { 100 | log.Infof(text) 101 | } 102 | } 103 | log.Infof("To still package %s without the required tools installed you need to run hover with the `--docker` flag.", t.packagingFormatName) 104 | return false 105 | } 106 | return true 107 | } 108 | 109 | func (t *packagingTask) Name() string { 110 | return t.packagingFormatName 111 | } 112 | 113 | func (t *packagingTask) Init() { 114 | t.init(false) 115 | } 116 | 117 | func (t *packagingTask) init(ignoreAlreadyExists bool) { 118 | for task := range t.dependsOn { 119 | task.init(true) 120 | } 121 | if !t.IsInitialized() { 122 | createPackagingFormatDirectory(t.packagingFormatName) 123 | dir := packagingFormatPath(t.packagingFormatName) 124 | for sourceFile, destinationFile := range t.templateFiles { 125 | destinationFile = filepath.Join(dir, destinationFile) 126 | err := os.MkdirAll(filepath.Dir(destinationFile), 0775) 127 | if err != nil { 128 | log.Errorf("Failed to create directory %s: %v", filepath.Dir(destinationFile), err) 129 | os.Exit(1) 130 | } 131 | fileutils.CopyAsset(fmt.Sprintf("packaging/%s", sourceFile), destinationFile) 132 | } 133 | if t.generateInitFiles != nil { 134 | log.Infof("Generating dynamic init files") 135 | t.generateInitFiles(config.GetConfig().GetPackageName(pubspec.GetPubSpec().Name), dir) 136 | } 137 | log.Infof("go/packaging/%s has been created. You can modify the configuration files and add it to git.", t.packagingFormatName) 138 | log.Infof(fmt.Sprintf("You now can package the %s using `%s`", strings.Split(t.packagingFormatName, "-")[0], log.Au().Magenta("hover build "+t.packagingFormatName))) 139 | } else if !ignoreAlreadyExists { 140 | log.Errorf("%s is already initialized for packaging.", t.packagingFormatName) 141 | os.Exit(1) 142 | } 143 | } 144 | 145 | func (t *packagingTask) Pack(fullVersion string, mode build.Mode) { 146 | projectName := pubspec.GetPubSpec().Name 147 | version := strings.Split(fullVersion, "+")[0] 148 | var release string 149 | if strings.Contains(fullVersion, "+") { 150 | release = strings.Split(fullVersion, "+")[1] 151 | } else { 152 | release = strings.ReplaceAll(fullVersion, ".", "") 153 | } 154 | description := pubspec.GetPubSpec().GetDescription() 155 | author := pubspec.GetPubSpec().GetAuthor() 156 | organizationName := config.GetConfig().GetOrganizationName() 157 | applicationName := config.GetConfig().GetApplicationName(projectName) 158 | executableName := config.GetConfig().GetExecutableName(projectName) 159 | packageName := config.GetConfig().GetPackageName(projectName) 160 | license := config.GetConfig().GetLicense() 161 | templateData := map[string]string{ 162 | "projectName": projectName, 163 | "version": version, 164 | "release": release, 165 | "description": description, 166 | "organizationName": organizationName, 167 | "author": author, 168 | "applicationName": applicationName, 169 | "executableName": executableName, 170 | "packageName": packageName, 171 | "license": license, 172 | } 173 | templateData["iconPath"] = executeStringTemplate(t.linuxDesktopFileIconPath, templateData) 174 | templateData["executablePath"] = executeStringTemplate(t.linuxDesktopFileExecutablePath, templateData) 175 | t.pack(templateData, packageName, projectName, applicationName, executableName, version, release, mode) 176 | } 177 | 178 | func (t *packagingTask) pack(templateData map[string]string, packageName, projectName, applicationName, executableName, version, release string, mode build.Mode) { 179 | if t.extraTemplateData != nil { 180 | for key, value := range t.extraTemplateData(packageName, packagingFormatPath(t.packagingFormatName)) { 181 | templateData[key] = value 182 | } 183 | } 184 | for task := range t.dependsOn { 185 | task.pack(templateData, packageName, projectName, applicationName, executableName, version, release, mode) 186 | } 187 | tmpPath := getTemporaryBuildDirectory(projectName, t.packagingFormatName) 188 | defer func() { 189 | err := os.RemoveAll(tmpPath) 190 | if err != nil { 191 | log.Errorf("Could not remove temporary build directory: %v", err) 192 | os.Exit(1) 193 | } 194 | }() 195 | log.Infof("Packaging %s in %s", strings.Split(t.packagingFormatName, "-")[1], tmpPath) 196 | 197 | if t.flutterBuildOutputDirectory != "" { 198 | err := copy.Copy(build.OutputDirectoryPath(strings.Split(t.packagingFormatName, "-")[0], mode), executeStringTemplate(filepath.Join(tmpPath, t.flutterBuildOutputDirectory), templateData)) 199 | if err != nil { 200 | log.Errorf("Could not copy build folder: %v", err) 201 | os.Exit(1) 202 | } 203 | } 204 | for task, destination := range t.dependsOn { 205 | err := copy.Copy(build.OutputDirectoryPath(task.packagingFormatName, mode), filepath.Join(tmpPath, destination)) 206 | if err != nil { 207 | log.Errorf("Could not copy build folder of %s: %v", task.packagingFormatName, err) 208 | os.Exit(1) 209 | } 210 | } 211 | fileutils.CopyTemplateDir(packagingFormatPath(t.packagingFormatName), filepath.Join(tmpPath), templateData) 212 | if t.generateBuildFiles != nil { 213 | log.Infof("Generating dynamic build files") 214 | t.generateBuildFiles(packageName, tmpPath) 215 | } 216 | 217 | for _, file := range t.executableFiles { 218 | err := os.Chmod(executeStringTemplate(filepath.Join(tmpPath, file), templateData), 0777) 219 | if err != nil { 220 | log.Errorf("Failed to change file permissions for %s file: %v", file, err) 221 | os.Exit(1) 222 | } 223 | } 224 | 225 | err := os.RemoveAll(build.OutputDirectoryPath(t.packagingFormatName, mode)) 226 | log.Printf("Cleaning the build directory") 227 | if err != nil { 228 | log.Errorf("Failed to clean output directory %s: %v", build.OutputDirectoryPath(t.packagingFormatName, mode), err) 229 | os.Exit(1) 230 | } 231 | 232 | relativeOutputFilePath, err := t.packagingFunction(tmpPath, applicationName, packageName, executableName, version, release) 233 | if err != nil { 234 | log.Errorf("%v", err) 235 | log.Warnf("Packaging is very experimental and has mostly been tested on Linux.") 236 | log.Infof("Please open an issue at https://github.com/go-flutter-desktop/go-flutter/issues/new?template=BUG.md") 237 | log.Infof("with the log and a reproducible example if possible. You may also zip your app code") 238 | log.Infof("if you are comfortable with it (closed source etc.) and attach it to the issue.") 239 | os.Exit(1) 240 | } 241 | outputFileName := filepath.Base(relativeOutputFilePath) 242 | outputFilePath := filepath.Join(build.OutputDirectoryPath(t.packagingFormatName, mode), outputFileName) 243 | err = copy.Copy(filepath.Join(tmpPath, relativeOutputFilePath), outputFilePath) 244 | if err != nil { 245 | log.Errorf("Could not move %s file: %v", outputFileName, err) 246 | os.Exit(1) 247 | } 248 | err = os.Chmod(outputFilePath, 0755) 249 | if err != nil { 250 | log.Errorf("Could not change file permissions for %s: %v", outputFileName, err) 251 | os.Exit(1) 252 | } 253 | } 254 | 255 | func (t *packagingTask) AssertInitialized() { 256 | if t.skipAssertInitialized { 257 | return 258 | } 259 | if !t.IsInitialized() { 260 | log.Errorf("%s is not initialized for packaging. Please run `hover init-packaging %s` first.", t.packagingFormatName, t.packagingFormatName) 261 | os.Exit(1) 262 | } 263 | } 264 | 265 | func (t *packagingTask) IsInitialized() bool { 266 | _, err := os.Stat(packagingFormatPath(t.packagingFormatName)) 267 | return !os.IsNotExist(err) 268 | } 269 | 270 | func executeStringTemplate(t string, data map[string]string) string { 271 | tmplFile, err := template.New("").Option("missingkey=error").Parse(t) 272 | if err != nil { 273 | log.Errorf("Failed to parse template string: %v\n", err) 274 | os.Exit(1) 275 | } 276 | var tmplBytes bytes.Buffer 277 | err = tmplFile.Execute(&tmplBytes, data) 278 | if err != nil { 279 | panic(err) 280 | } 281 | return tmplBytes.String() 282 | } 283 | -------------------------------------------------------------------------------- /cmd/packaging/task.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import "github.com/go-flutter-desktop/hover/internal/build" 4 | 5 | // Task contains all configuration options for a given packaging method. 6 | // TODO: Rename to something that suits it more? Mabe Executor? 7 | type Task interface { 8 | Name() string 9 | Init() 10 | IsInitialized() bool 11 | AssertInitialized() 12 | Pack(buildVersion string, mode build.Mode) 13 | IsSupported() bool 14 | AssertSupported() 15 | } 16 | -------------------------------------------------------------------------------- /cmd/packaging/windows-msi.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "image/png" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | 15 | ico "github.com/Kodeworks/golang-image-ico" 16 | "github.com/google/uuid" 17 | 18 | "github.com/go-flutter-desktop/hover/internal/log" 19 | ) 20 | 21 | var directoriesFileContent []string 22 | var directoryRefsFileContent []string 23 | var componentRefsFileContent []string 24 | 25 | // WindowsMsiTask packaging for windows as msi 26 | var WindowsMsiTask = &packagingTask{ 27 | packagingFormatName: "windows-msi", 28 | templateFiles: map[string]string{ 29 | "windows-msi/app.wxs.tmpl": "{{.packageName}}.wxs.tmpl", 30 | }, 31 | flutterBuildOutputDirectory: "build", 32 | packagingFunction: func(tmpPath, applicationName, packageName, executableName, version, release string) (string, error) { 33 | outputFileName := fmt.Sprintf("%s %s.msi", applicationName, version) 34 | iconPngFile, err := os.Open(filepath.Join(tmpPath, "build", "assets", "icon.png")) 35 | if err != nil { 36 | return "", err 37 | } 38 | pngImage, err := png.Decode(iconPngFile) 39 | if err != nil { 40 | return "", err 41 | } 42 | // We can't defer it, because windows reports that the file is used by another program 43 | err = iconPngFile.Close() 44 | if err != nil { 45 | return "", err 46 | } 47 | iconIcoFile, err := os.Create(filepath.Join(tmpPath, "build", "assets", "icon.ico")) 48 | if err != nil { 49 | return "", err 50 | } 51 | err = ico.Encode(iconIcoFile, pngImage) 52 | if err != nil { 53 | return "", err 54 | } 55 | // We can't defer it, because windows reports that the file is used by another program 56 | err = iconIcoFile.Close() 57 | if err != nil { 58 | return "", err 59 | } 60 | switch runtime.GOOS { 61 | case "windows": 62 | cmdCandle := exec.Command("candle", fmt.Sprintf("%s.wxs", packageName)) 63 | cmdCandle.Dir = tmpPath 64 | cmdCandle.Stdout = os.Stdout 65 | cmdCandle.Stderr = os.Stderr 66 | err = cmdCandle.Run() 67 | if err != nil { 68 | return "", err 69 | } 70 | cmdLight := exec.Command("light", fmt.Sprintf("%s.wixobj", packageName), "-sval") 71 | cmdLight.Dir = tmpPath 72 | cmdLight.Stdout = os.Stdout 73 | cmdLight.Stderr = os.Stderr 74 | err = cmdLight.Run() 75 | if err != nil { 76 | return "", err 77 | } 78 | err = os.Rename(filepath.Join(tmpPath, fmt.Sprintf("%s.msi", packageName)), filepath.Join(tmpPath, outputFileName)) 79 | if err != nil { 80 | return "", err 81 | } 82 | case "linux": 83 | cmdWixl := exec.Command("wixl", "-v", fmt.Sprintf("%s.wxs", packageName), "-o", outputFileName) 84 | cmdWixl.Dir = tmpPath 85 | cmdWixl.Stdout = os.Stdout 86 | cmdWixl.Stderr = os.Stderr 87 | err = cmdWixl.Run() 88 | if err != nil { 89 | return "", err 90 | } 91 | default: 92 | panic("should be unreachable") 93 | } 94 | return outputFileName, nil 95 | }, 96 | requiredTools: map[string]map[string]string{ 97 | "windows": { 98 | "candle": "Install the WiX Toolset from https://wixtoolset.org/releases/", // Only for one tool, because displaying the message twice makes no sense 99 | }, 100 | "linux": { 101 | "wixl": "Install msitools from your package manager or from https://wiki.gnome.org/msitools/", 102 | }, 103 | }, 104 | generateInitFiles: func(packageName, path string) { 105 | err := ioutil.WriteFile( 106 | filepath.Join(path, "upgrade-code.txt"), 107 | []byte(fmt.Sprintf("%s\n# This GUID is your upgrade code and ensures that you can properly update your app.\n# Don't change it.", uuid.New())), 108 | 0755, 109 | ) 110 | if err != nil { 111 | log.Errorf("Failed to create `upgrade-code.txt` file: %v", err) 112 | os.Exit(1) 113 | } 114 | }, 115 | extraTemplateData: func(packageName, path string) map[string]string { 116 | data, err := ioutil.ReadFile(filepath.Join(path, "upgrade-code.txt")) 117 | if err != nil { 118 | log.Errorf("Failed to read `go/packaging/windows-msi/upgrade-code.txt`: %v", err) 119 | if os.IsNotExist(err) { 120 | log.Errorf("Please re-init windows-msi to generate the `go/packaging/windows-msi/upgrade-code.txt`") 121 | log.Errorf("or put a GUID from https://www.guidgen.com/ into a new `go/packaging/windows-msi/upgrade-code.txt` file.") 122 | } 123 | os.Exit(1) 124 | } 125 | guid := strings.Split(string(data), "\n")[0] 126 | return map[string]string{ 127 | "upgradeCode": guid, 128 | "pathSeparator": string(os.PathSeparator), 129 | } 130 | }, 131 | generateBuildFiles: func(packageName, tmpPath string) { 132 | directoriesFilePath, err := filepath.Abs(filepath.Join(tmpPath, "directories.wxi")) 133 | if err != nil { 134 | log.Errorf("Failed to resolve absolute path for directories.wxi file %s: %v", packageName, err) 135 | os.Exit(1) 136 | } 137 | directoriesFile, err := os.Create(directoriesFilePath) 138 | if err != nil { 139 | log.Errorf("Failed to create directories.wxi file %s: %v", packageName, err) 140 | os.Exit(1) 141 | } 142 | directoryRefsFilePath, err := filepath.Abs(filepath.Join(tmpPath, "directory_refs.wxi")) 143 | if err != nil { 144 | log.Errorf("Failed to resolve absolute path for directory_refs.wxi file %s: %v", packageName, err) 145 | os.Exit(1) 146 | } 147 | directoryRefsFile, err := os.Create(directoryRefsFilePath) 148 | if err != nil { 149 | log.Errorf("Failed to create directory_refs.wxi file %s: %v", packageName, err) 150 | os.Exit(1) 151 | } 152 | componentRefsFilePath, err := filepath.Abs(filepath.Join(tmpPath, "component_refs.wxi")) 153 | if err != nil { 154 | log.Errorf("Failed to resolve absolute path for component_refs.wxi file %s: %v", packageName, err) 155 | os.Exit(1) 156 | } 157 | componentRefsFile, err := os.Create(componentRefsFilePath) 158 | if err != nil { 159 | log.Errorf("Failed to create component_refs.wxi file %s: %v", packageName, err) 160 | os.Exit(1) 161 | } 162 | directoriesFileContent = append(directoriesFileContent, "") 163 | directoryRefsFileContent = append(directoryRefsFileContent, "") 164 | componentRefsFileContent = append(componentRefsFileContent, "") 165 | windowsMsiProcessFiles(filepath.Join(tmpPath, "build")) 166 | directoriesFileContent = append(directoriesFileContent, "") 167 | directoryRefsFileContent = append(directoryRefsFileContent, "") 168 | componentRefsFileContent = append(componentRefsFileContent, "") 169 | 170 | for _, line := range directoriesFileContent { 171 | if _, err := directoriesFile.WriteString(line + "\n"); err != nil { 172 | log.Errorf("Could not write directories.wxi: %v", packageName, err) 173 | os.Exit(1) 174 | } 175 | } 176 | err = directoriesFile.Close() 177 | if err != nil { 178 | log.Errorf("Could not close directories.wxi: %v", packageName, err) 179 | os.Exit(1) 180 | } 181 | for _, line := range directoryRefsFileContent { 182 | if _, err := directoryRefsFile.WriteString(line + "\n"); err != nil { 183 | log.Errorf("Could not write directory_refs.wxi: %v", packageName, err) 184 | os.Exit(1) 185 | } 186 | } 187 | err = directoryRefsFile.Close() 188 | if err != nil { 189 | log.Errorf("Could not close directory_refs.wxi: %v", packageName, err) 190 | os.Exit(1) 191 | } 192 | for _, line := range componentRefsFileContent { 193 | if _, err := componentRefsFile.WriteString(line + "\n"); err != nil { 194 | log.Errorf("Could not write component_refs.wxi: %v", packageName, err) 195 | os.Exit(1) 196 | } 197 | } 198 | err = componentRefsFile.Close() 199 | if err != nil { 200 | log.Errorf("Could not close component_refs.wxi: %v", packageName, err) 201 | os.Exit(1) 202 | } 203 | }, 204 | } 205 | 206 | func windowsMsiProcessFiles(path string) { 207 | pathSeparator := string(os.PathSeparator) 208 | files, err := ioutil.ReadDir(path) 209 | if err != nil { 210 | log.Errorf("Failed to read directory %s: %v", path, err) 211 | os.Exit(1) 212 | } 213 | 214 | for _, f := range files { 215 | p := filepath.Join(path, f.Name()) 216 | relativePath := strings.Split(strings.Split(p, "build"+pathSeparator)[1], pathSeparator) 217 | id := hashSha1(strings.Join(relativePath, "")) 218 | if f.IsDir() { 219 | directoriesFileContent = append(directoriesFileContent, 220 | fmt.Sprintf(``, id, f.Name()), 221 | ) 222 | windowsMsiProcessFiles(p) 223 | directoriesFileContent = append(directoriesFileContent, 224 | "", 225 | ) 226 | } else { 227 | if len(relativePath) > 1 { 228 | directoryRefsFileContent = append(directoryRefsFileContent, 229 | fmt.Sprintf(``, hashSha1(strings.Join(relativePath[:len(relativePath)-1], ""))), 230 | ) 231 | } else { 232 | directoryRefsFileContent = append(directoryRefsFileContent, 233 | ``, 234 | ) 235 | } 236 | fileSource := filepath.Join("build", strings.Join(relativePath, pathSeparator)) 237 | directoryRefsFileContent = append(directoryRefsFileContent, 238 | fmt.Sprintf(``, id, uuid.New()), 239 | fmt.Sprintf(``, id, fileSource), 240 | "", 241 | "", 242 | ) 243 | componentRefsFileContent = append(componentRefsFileContent, 244 | fmt.Sprintf(``, id), 245 | ) 246 | } 247 | } 248 | } 249 | 250 | func hashSha1(content string) string { 251 | h := sha1.New() 252 | h.Write([]byte(content)) 253 | sha := h.Sum(nil) 254 | return hex.EncodeToString(sha) 255 | } 256 | -------------------------------------------------------------------------------- /cmd/plugins.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | "time" 15 | 16 | "github.com/pkg/errors" 17 | "github.com/spf13/cobra" 18 | "golang.org/x/mod/modfile" 19 | "gopkg.in/yaml.v2" 20 | 21 | "github.com/go-flutter-desktop/hover/internal/build" 22 | "github.com/go-flutter-desktop/hover/internal/fileutils" 23 | "github.com/go-flutter-desktop/hover/internal/log" 24 | "github.com/go-flutter-desktop/hover/internal/modx" 25 | "github.com/go-flutter-desktop/hover/internal/pubspec" 26 | ) 27 | 28 | const standaloneImplementationListAPI = "https://raw.githubusercontent.com/go-flutter-desktop/plugins/master/list.json" 29 | 30 | var ( 31 | listAllPluginDependencies bool 32 | tidyPurge bool 33 | dryRun bool 34 | reImport bool 35 | ) 36 | 37 | func init() { 38 | pluginTidyCmd.Flags().BoolVar(&tidyPurge, "purge", false, "Remove all go platform plugins imports from the project.") 39 | pluginListCmd.Flags().BoolVarP(&listAllPluginDependencies, "all", "a", false, "List all platform plugins dependencies, even the one have no go-flutter support") 40 | pluginGetCmd.Flags().BoolVar(&reImport, "force", false, "Re-import already imported plugins.") 41 | 42 | pluginCmd.PersistentFlags().BoolVarP(&dryRun, "dry-run", "n", false, "Perform a trial run with no changes made.") 43 | 44 | pluginCmd.AddCommand(pluginListCmd) 45 | pluginCmd.AddCommand(pluginGetCmd) 46 | pluginCmd.AddCommand(pluginTidyCmd) 47 | rootCmd.AddCommand(pluginCmd) 48 | } 49 | 50 | var pluginCmd = &cobra.Command{ 51 | Use: "plugins", 52 | Short: "Tools for plugins", 53 | Long: "A collection of commands to help with finding/importing go-flutter implementations of plugins.", 54 | } 55 | 56 | // PubSpecLock contains the parsed contents of pubspec.lock 57 | type PubSpecLock struct { 58 | Packages map[string]PubDep 59 | } 60 | 61 | // PubDep contains one entry of the pubspec.lock yaml list 62 | type PubDep struct { 63 | Dependency string 64 | Description interface{} 65 | Source string 66 | Version string 67 | 68 | // Fields set by hover 69 | name string 70 | android bool 71 | ios bool 72 | desktop bool 73 | // optional description values 74 | path string // correspond to the path field in lock file 75 | host string // correspond to the host field in lock file 76 | // contain a import.go.tmpl file used for import 77 | autoImport bool 78 | // the path/URL to the go code of the plugin is stored 79 | pluginGoSource string 80 | // whether or not the go plugin source code is located on another VCS repo. 81 | standaloneImpl bool 82 | } 83 | 84 | func (p PubDep) imported() bool { 85 | pluginImportOutPath := filepath.Join(build.BuildPath, "cmd", fmt.Sprintf("import-%s-plugin.go", p.name)) 86 | if _, err := os.Stat(pluginImportOutPath); err == nil { 87 | return true 88 | } 89 | return false 90 | } 91 | 92 | func (p PubDep) platforms() []string { 93 | var platforms []string 94 | if p.android { 95 | platforms = append(platforms, "android") 96 | } 97 | if p.ios { 98 | platforms = append(platforms, "ios") 99 | } 100 | if p.desktop { 101 | platforms = append(platforms, build.BuildPath) 102 | } 103 | return platforms 104 | } 105 | 106 | var pluginListCmd = &cobra.Command{ 107 | Use: "list", 108 | Short: "List golang platform plugins in the application", 109 | Args: func(cmd *cobra.Command, args []string) error { 110 | if len(args) > 0 { 111 | return errors.New("does not take arguments") 112 | } 113 | return nil 114 | }, 115 | Run: func(cmd *cobra.Command, args []string) { 116 | assertInFlutterProject() 117 | dependencyList, err := listPlatformPlugin() 118 | if err != nil { 119 | log.Errorf("%v", err) 120 | os.Exit(1) 121 | } 122 | 123 | var hasNewPlugin bool 124 | var hasPlugins bool 125 | for _, dep := range dependencyList { 126 | if !(dep.desktop || listAllPluginDependencies) { 127 | continue 128 | } 129 | 130 | if hasPlugins { 131 | fmt.Println("") 132 | } 133 | hasPlugins = true 134 | 135 | log.Infof(" - %s", dep.name) 136 | log.Infof(" version: %s", dep.Version) 137 | log.Infof(" platforms: [%s]", strings.Join(dep.platforms(), ", ")) 138 | if dep.desktop { 139 | if dep.standaloneImpl { 140 | log.Infof(" source: This go plugin isn't maintained by the official plugin creator.") 141 | } 142 | if dep.imported() { 143 | log.Infof(" import: [OK] The plugin is already imported in the project.") 144 | continue 145 | } 146 | if dep.autoImport || dep.standaloneImpl { 147 | hasNewPlugin = true 148 | log.Infof(" import: [Missing] The plugin can be imported by hover.") 149 | } else { 150 | log.Infof(" import: [Manual import] The plugin is missing the import.go.tmpl file required for hover import.") 151 | } 152 | if dep.path != "" { 153 | log.Infof(" dev: Plugin replaced in go.mod to path: '%s'", dep.path) 154 | } 155 | } 156 | } 157 | if hasNewPlugin { 158 | log.Infof(fmt.Sprintf("run `%s` to import the missing plugins!", log.Au().Magenta("hover plugins get"))) 159 | } 160 | }, 161 | } 162 | 163 | var pluginTidyCmd = &cobra.Command{ 164 | Use: "tidy", 165 | Short: "Removes unused platform plugins.", 166 | Args: func(cmd *cobra.Command, args []string) error { 167 | if len(args) > 0 { 168 | return errors.New("does not take arguments") 169 | } 170 | return nil 171 | }, 172 | Run: func(cmd *cobra.Command, args []string) { 173 | var ( 174 | err error 175 | gomod *modfile.File 176 | ) 177 | assertInFlutterProject() 178 | assertHoverInitialized() 179 | 180 | gomod, err = modx.Open(build.BuildPath) 181 | if err != nil { 182 | log.Errorf("failed to open go.mod", err) 183 | os.Exit(1) 184 | } 185 | 186 | desktopCmdPath := filepath.Join(build.BuildPath, "cmd") 187 | dependencyList, err := listPlatformPlugin() 188 | if err != nil { 189 | log.Errorf("%v", err) 190 | os.Exit(1) 191 | } 192 | 193 | importedPlugins, err := ioutil.ReadDir(desktopCmdPath) 194 | if err != nil { 195 | log.Errorf("Failed to search for plugins: %v", err) 196 | os.Exit(1) 197 | } 198 | 199 | for _, f := range importedPlugins { 200 | isPlugin := strings.HasPrefix(f.Name(), "import-") 201 | isPlugin = isPlugin && strings.HasSuffix(f.Name(), "-plugin.go") 202 | 203 | if isPlugin { 204 | pluginName := strings.TrimPrefix(f.Name(), "import-") 205 | pluginName = strings.TrimSuffix(pluginName, "-plugin.go") 206 | pluginImportPath := filepath.Join(desktopCmdPath, f.Name()) 207 | pluginInUse := false 208 | 209 | for _, dep := range dependencyList { 210 | if dep.name == pluginName { 211 | // plugin in pubspec.lock 212 | pluginInUse = true 213 | break 214 | } 215 | } 216 | 217 | if !pluginInUse || tidyPurge { 218 | // clean-up go.mod 219 | pluginImportStr, err := readPluginGoImport(pluginImportPath, pluginName) 220 | 221 | // Delete the 'replace' and 'require' import strings from go.mod. 222 | // Not mission critical, if the plugins not correctly removed from 223 | // the go.mod file, the project still works and the plugin is 224 | // successfully removed from the flutter.Application. 225 | if err != nil || pluginImportStr == "" { 226 | log.Warnf("Couldn't clean the '%s' plugin from the 'go.mod' file. Error: %v", pluginName, err) 227 | } else { 228 | if err = modx.RemoveModule(gomod, pluginImportStr); err != nil { 229 | log.Warnf("failed remove %s from %s/go.mod: %v", pluginImportStr, build.BuildPath, err) 230 | } 231 | } 232 | 233 | // remove import file 234 | if !dryRun { 235 | if err = os.Remove(pluginImportPath); err != nil { 236 | log.Warnf("Couldn't remove plugin %s: %v", pluginName, err) 237 | continue 238 | } 239 | } 240 | log.Infof(" plugin: [%s] removed", pluginName) 241 | } 242 | } 243 | } 244 | 245 | if dryRun { 246 | s, err := modx.Print(gomod) 247 | if err != nil { 248 | log.Errorf("failed to print updated go.mod: %v", err) 249 | os.Exit(1) 250 | } 251 | 252 | log.Infof("modified go.mod:\n%s", s) 253 | } else { 254 | err = modx.Replace(build.BuildPath, gomod) 255 | if err != nil { 256 | log.Errorf("failed to update go.mod: %v", err) 257 | os.Exit(1) 258 | } 259 | } 260 | 261 | if tidyPurge { 262 | intermediatesDirectoryPath, err := filepath.Abs(filepath.Join(build.BuildPath, "build", "intermediates")) 263 | if err != nil { 264 | log.Errorf("Failed to resolve absolute path for intermediates directory: %v", err) 265 | os.Exit(1) 266 | } 267 | if fileutils.IsDirectory(intermediatesDirectoryPath) { 268 | _ = os.RemoveAll(intermediatesDirectoryPath) 269 | } 270 | } 271 | }, 272 | } 273 | 274 | var pluginGetCmd = &cobra.Command{ 275 | Use: "get", 276 | Short: "Imports missing platform plugins in the application", 277 | Args: func(cmd *cobra.Command, args []string) error { 278 | if len(args) > 0 { 279 | return errors.New("does not take arguments") 280 | } 281 | return nil 282 | }, 283 | Run: func(cmd *cobra.Command, args []string) { 284 | assertInFlutterProject() 285 | assertHoverInitialized() 286 | hoverPluginGet(false) 287 | }, 288 | } 289 | 290 | func hoverPluginGet(dryRun bool) bool { 291 | dependencyList, err := listPlatformPlugin() 292 | if err != nil { 293 | log.Errorf("%v", err) 294 | os.Exit(1) 295 | } 296 | 297 | for _, dep := range dependencyList { 298 | if !dep.desktop { 299 | continue 300 | } 301 | 302 | if !dep.autoImport { 303 | log.Infof(" plugin: [%s] couldn't be imported, check the plugin's README for manual instructions", dep.name) 304 | continue 305 | } 306 | 307 | if dryRun { 308 | if dep.imported() { 309 | log.Infof(" plugin: [%s] can be updated", dep.name) 310 | } else { 311 | log.Infof(" plugin: [%s] can be imported", dep.name) 312 | } 313 | continue 314 | } 315 | 316 | pluginImportOutPath := filepath.Join(build.BuildPath, "cmd", fmt.Sprintf("import-%s-plugin.go", dep.name)) 317 | if dep.imported() && !reImport { 318 | pluginImportStr, err := readPluginGoImport(pluginImportOutPath, dep.name) 319 | if err != nil { 320 | log.Warnf("Couldn't read the plugin '%s' import URL", dep.name) 321 | log.Warnf("Fallback to the latest version installed.") 322 | continue 323 | } 324 | 325 | if !goGetModuleSuccess(pluginImportStr, dep.Version) { 326 | log.Warnf("Couldn't download version '%s' of plugin '%s'", dep.Version, dep.name) 327 | log.Warnf("Fallback to the latest version installed.") 328 | continue 329 | } 330 | 331 | log.Infof(" plugin: [%s] updated", dep.name) 332 | continue 333 | } 334 | 335 | if dep.standaloneImpl { 336 | fileutils.DownloadFile(dep.pluginGoSource, pluginImportOutPath) 337 | } else { 338 | autoImportTemplatePath := filepath.Join(dep.pluginGoSource, "import.go.tmpl") 339 | fileutils.CopyFile(autoImportTemplatePath, pluginImportOutPath) 340 | 341 | if fileutils.IsDirectory(filepath.Join(dep.pluginGoSource, "dlib")) { 342 | dlibPath, err := filepath.Abs(filepath.Join(dep.pluginGoSource, "dlib")) 343 | if err != nil { 344 | log.Errorf("Failed to resolve absolute path for dlib directory: %v", err) 345 | os.Exit(1) 346 | } 347 | 348 | intermediatesDirectoryPath, err := filepath.Abs(filepath.Join(build.BuildPath, "build", "intermediates")) 349 | if err != nil { 350 | log.Errorf("Failed to resolve absolute path for intermediates directory: %v", err) 351 | os.Exit(1) 352 | } 353 | 354 | fileutils.CopyDir(dlibPath, intermediatesDirectoryPath) 355 | if fileutils.IsFileExists(filepath.Join(dlibPath, "README.md")) { 356 | readmeName := fmt.Sprintf("README-%s.md", dep.name) 357 | fileutils.CopyFile(filepath.Join(dlibPath, "README.md"), filepath.Join(intermediatesDirectoryPath, readmeName)) 358 | _ = os.Remove(filepath.Join(intermediatesDirectoryPath, "README.md")) 359 | } 360 | } 361 | 362 | pluginImportStr, err := readPluginGoImport(pluginImportOutPath, dep.name) 363 | if err != nil { 364 | log.Warnf("Couldn't read the plugin '%s' import URL", dep.name) 365 | log.Warnf("Fallback to the latest version available on github.") 366 | continue 367 | } 368 | 369 | // if remote plugin, get the correct version 370 | if dep.path == "" { 371 | if !goGetModuleSuccess(pluginImportStr, dep.Version) { 372 | log.Warnf("Couldn't download version '%s' of plugin '%s'", dep.Version, dep.name) 373 | log.Warnf("Fallback to the latest version available on github.") 374 | } 375 | } 376 | 377 | // if local plugin 378 | if dep.path != "" { 379 | path, err := filepath.Abs(filepath.Join(dep.path, build.BuildPath)) 380 | if err != nil { 381 | log.Errorf("Failed to resolve absolute path for plugin '%s': %v", dep.name, err) 382 | os.Exit(1) 383 | } 384 | 385 | // mutation we're applying to go.mod 386 | mut := func(gomod *modfile.File) error { 387 | return gomod.AddReplace(pluginImportStr, "", path, "") 388 | } 389 | 390 | if err = modx.Mutate(build.BuildPath, mut); err != nil { 391 | log.Errorf("failed to update go.mod: %v", err) 392 | os.Exit(1) 393 | } 394 | } 395 | 396 | log.Infof(" plugin: [%s] imported", dep.name) 397 | } 398 | } 399 | 400 | return len(dependencyList) != 0 401 | } 402 | 403 | func listPlatformPlugin() ([]PubDep, error) { 404 | onlineList, err := fetchStandaloneImplementationList() 405 | if err != nil { 406 | log.Warnf("Warning, couldn't read the online plugin list: %v", err) 407 | } 408 | 409 | pubcachePath, err := findPubcachePath() 410 | if err != nil { 411 | return nil, errors.Wrap(err, "failed to find path for pub-cache") 412 | } 413 | 414 | var list []PubDep 415 | pubLock, err := readPubSpecLock() 416 | if err != nil { 417 | log.Infof("Run `%s` (or equivalent) first", log.Au().Magenta("flutter build bundle")) 418 | return nil, err 419 | } 420 | 421 | for name, entry := range pubLock.Packages { 422 | entry.name = name 423 | 424 | switch i := entry.Description.(type) { 425 | case string: 426 | if i == "flutter" { 427 | continue 428 | } 429 | case map[interface{}]interface{}: 430 | if value, ok := i["path"]; ok { 431 | entry.path = value.(string) 432 | } 433 | if value, ok := i["url"]; ok { 434 | url, err := url.Parse(value.(string)) 435 | if err != nil { 436 | return nil, errors.Wrap(err, "failed to parse URL from string %s"+value.(string)) 437 | } 438 | entry.host = url.Host 439 | } 440 | } 441 | 442 | pluginPath := filepath.Join(pubcachePath, "hosted", entry.host, entry.name+"-"+entry.Version) 443 | if entry.path != "" { 444 | pluginPath = entry.path 445 | } 446 | 447 | pluginPubspecPath := filepath.Join(pluginPath, "pubspec.yaml") 448 | pluginPubspec, err := pubspec.ReadPubSpecFile(pluginPubspecPath) 449 | if err != nil { 450 | continue 451 | } 452 | 453 | // Non plugin package are likely to contain android/ios folders (even 454 | // through they aren't used). 455 | // To check if the package is really a platform plugin, we need to read 456 | // the pubspec.yaml file. If he contains a Flutter/plugin entry, then 457 | // it's a platform plugin. 458 | if _, ok := pluginPubspec.Flutter["plugin"]; !ok { 459 | continue 460 | } 461 | 462 | detectPlatformPlugin := func(platform string) (bool, error) { 463 | platformPath := filepath.Join(pluginPath, platform) 464 | stat, err := os.Stat(platformPath) 465 | if err != nil { 466 | if os.IsNotExist(err) { 467 | return false, nil 468 | } 469 | return false, errors.Wrapf(err, "failed to stat %s", platformPath) 470 | } 471 | return stat.IsDir(), nil 472 | } 473 | 474 | entry.android, err = detectPlatformPlugin("android") 475 | if err != nil { 476 | return nil, err 477 | } 478 | entry.ios, err = detectPlatformPlugin("ios") 479 | if err != nil { 480 | return nil, err 481 | } 482 | entry.desktop, err = detectPlatformPlugin(build.BuildPath) 483 | if err != nil { 484 | return nil, err 485 | } 486 | 487 | if entry.desktop { 488 | entry.pluginGoSource = filepath.Join(pluginPath, build.BuildPath) 489 | autoImportTemplate := filepath.Join(entry.pluginGoSource, "import.go.tmpl") 490 | _, err := os.Stat(autoImportTemplate) 491 | entry.autoImport = true 492 | if err != nil { 493 | entry.autoImport = false 494 | if !os.IsNotExist(err) { 495 | return nil, errors.Wrapf(err, "failed to stat %s", autoImportTemplate) 496 | } 497 | } 498 | } else { 499 | // check if the plugin is available in github.com/go-flutter-desktop/plugins 500 | for _, plugin := range onlineList { 501 | if entry.name == plugin.Name { 502 | entry.desktop = true 503 | entry.standaloneImpl = true 504 | entry.autoImport = true 505 | entry.pluginGoSource = plugin.ImportFile 506 | break 507 | } 508 | } 509 | } 510 | 511 | list = append(list, entry) 512 | 513 | } 514 | return list, nil 515 | } 516 | 517 | // readLocal reads pubspec.lock in the current working directory. 518 | func readPubSpecLock() (*PubSpecLock, error) { 519 | p := &PubSpecLock{} 520 | file, err := os.Open("pubspec.lock") 521 | if err != nil { 522 | if os.IsNotExist(err) { 523 | return nil, errors.New("no pubspec.lock file found") 524 | 525 | } 526 | return nil, errors.Wrap(err, "failed to open pubspec.lock") 527 | } 528 | defer file.Close() 529 | 530 | err = yaml.NewDecoder(file).Decode(p) 531 | if err != nil { 532 | return nil, errors.Wrap(err, "failed to decode pubspec.lock") 533 | } 534 | return p, nil 535 | } 536 | 537 | func readPluginGoImport(pluginImportOutPath, pluginName string) (string, error) { 538 | pluginImportBytes, err := ioutil.ReadFile(pluginImportOutPath) 539 | if err != nil && !os.IsNotExist(err) { 540 | return "", err 541 | } 542 | 543 | re := regexp.MustCompile(fmt.Sprintf(`\s+%s\s"(\S*)"`, pluginName)) 544 | 545 | match := re.FindStringSubmatch(string(pluginImportBytes)) 546 | if len(match) < 2 { 547 | err = errors.New("Failed to parse the import path, plugin name in the import must have been changed") 548 | return "", err 549 | } 550 | return match[1], nil 551 | } 552 | 553 | type onlineList struct { 554 | List []StandaloneImplementation `json:"standaloneImplementation"` 555 | } 556 | 557 | // StandaloneImplementation contains the go-flutter compatible plugins that 558 | // aren't merged into original VSC repo. 559 | type StandaloneImplementation struct { 560 | Name string `json:"name"` 561 | ImportFile string `json:"importFile"` 562 | } 563 | 564 | func fetchStandaloneImplementationList() ([]StandaloneImplementation, error) { 565 | remoteList := &onlineList{} 566 | 567 | client := http.Client{ 568 | Timeout: time.Second * 20, // Maximum of 10 secs 569 | } 570 | 571 | req, err := http.NewRequest(http.MethodGet, standaloneImplementationListAPI, nil) 572 | if err != nil { 573 | return remoteList.List, err 574 | } 575 | 576 | res, err := client.Do(req) 577 | if err != nil { 578 | return remoteList.List, err 579 | } 580 | 581 | body, err := ioutil.ReadAll(res.Body) 582 | if err != nil { 583 | return remoteList.List, err 584 | } 585 | 586 | if res.StatusCode != 200 { 587 | return remoteList.List, errors.New(strings.TrimRight(string(body), "\r\n")) 588 | } 589 | 590 | err = json.Unmarshal(body, remoteList) 591 | if err != nil { 592 | return remoteList.List, err 593 | } 594 | return remoteList.List, nil 595 | } 596 | 597 | // goGetModuleSuccess updates a module at a version, if it fails, return false. 598 | func goGetModuleSuccess(pluginImportStr, version string) bool { 599 | cmdGoGetU := exec.Command(build.GoBin(), "get", "-u", "-d", pluginImportStr+"@v"+version) 600 | cmdGoGetU.Dir = filepath.Join(build.BuildPath) 601 | cmdGoGetU.Env = append(os.Environ(), 602 | "GOPROXY=direct", // github.com/golang/go/issues/32955 (allows '/' in branch name) 603 | "GO111MODULE=on", 604 | ) 605 | cmdGoGetU.Stderr = os.Stderr 606 | cmdGoGetU.Stdout = os.Stdout 607 | return cmdGoGetU.Run() == nil 608 | } 609 | -------------------------------------------------------------------------------- /cmd/prepare-engine.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/go-flutter-desktop/hover/internal/build" 8 | "github.com/go-flutter-desktop/hover/internal/config" 9 | "github.com/go-flutter-desktop/hover/internal/enginecache" 10 | "github.com/go-flutter-desktop/hover/internal/log" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | prepareCachePath string 16 | prepareEngineVersion string 17 | prepareReleaseMode bool 18 | prepareDebugMode bool 19 | prepareProfileMode bool 20 | prepareBuildModes []build.Mode 21 | ) 22 | 23 | func init() { 24 | prepareEngineCmd.PersistentFlags().StringVar(&prepareCachePath, "cache-path", enginecache.DefaultCachePath(), "The path that hover uses to cache dependencies such as the Flutter engine .so/.dll") 25 | prepareEngineCmd.PersistentFlags().StringVar(&prepareEngineVersion, "engine-version", config.BuildEngineDefault, "The flutter engine version to use.") 26 | prepareEngineCmd.PersistentFlags().BoolVar(&prepareDebugMode, "debug", false, "Prepare the flutter engine for debug mode") 27 | prepareEngineCmd.PersistentFlags().BoolVar(&prepareReleaseMode, "release", false, "Prepare the flutter engine for release mode.") 28 | prepareEngineCmd.PersistentFlags().BoolVar(&prepareProfileMode, "profile", false, "Prepare the flutter engine for profile mode.") 29 | prepareEngineCmd.AddCommand(prepareEngineLinuxCmd) 30 | prepareEngineCmd.AddCommand(prepareEngineDarwinCmd) 31 | prepareEngineCmd.AddCommand(prepareEngineWindowsCmd) 32 | rootCmd.AddCommand(prepareEngineCmd) 33 | } 34 | 35 | var prepareEngineCmd = &cobra.Command{ 36 | Use: "prepare-engine", 37 | Short: "Validates or updates the flutter engine on this machine for a given platform", 38 | } 39 | 40 | var prepareEngineLinuxCmd = &cobra.Command{ 41 | Use: "linux", 42 | Short: "Validates or updates the flutter engine on this machine for a given platform", 43 | Run: func(cmd *cobra.Command, args []string) { 44 | initPrepareEngineParameters("linux") 45 | subcommandPrepare("linux") 46 | }, 47 | } 48 | 49 | var prepareEngineDarwinCmd = &cobra.Command{ 50 | Use: "darwin", 51 | Short: "Validates or updates the flutter engine on this machine for a given platform", 52 | Run: func(cmd *cobra.Command, args []string) { 53 | initPrepareEngineParameters("darwin") 54 | subcommandPrepare("darwin") 55 | }, 56 | } 57 | 58 | var prepareEngineWindowsCmd = &cobra.Command{ 59 | Use: "windows", 60 | Short: "Validates or updates the flutter engine on this machine for a given platform", 61 | Run: func(cmd *cobra.Command, args []string) { 62 | initPrepareEngineParameters("windows") 63 | subcommandPrepare("windows") 64 | }, 65 | } 66 | 67 | func initPrepareEngineParameters(targetOS string) { 68 | validatePrepareEngineParameters(targetOS) 69 | if prepareDebugMode { 70 | prepareBuildModes = append(prepareBuildModes, build.DebugMode) 71 | } 72 | if prepareReleaseMode { 73 | prepareBuildModes = append(prepareBuildModes, build.ReleaseMode) 74 | } 75 | if prepareProfileMode { 76 | prepareBuildModes = append(prepareBuildModes, build.ProfileMode) 77 | } 78 | } 79 | 80 | func validatePrepareEngineParameters(targetOS string) { 81 | numberOfPrepareModeFlagsSet := 0 82 | for _, flag := range []bool{prepareProfileMode, prepareProfileMode, prepareDebugMode} { 83 | if flag { 84 | numberOfPrepareModeFlagsSet++ 85 | } 86 | } 87 | if numberOfPrepareModeFlagsSet > 1 { 88 | log.Errorf("Only one of --debug, --release or --profile can be set at one time") 89 | os.Exit(1) 90 | } 91 | if numberOfPrepareModeFlagsSet == 0 { 92 | prepareDebugMode = true 93 | } 94 | if targetOS == "darwin" && runtime.GOOS != targetOS && (prepareReleaseMode || prepareProfileMode) { 95 | log.Errorf("It is not possible to prepare the flutter engine in release mode for darwin using docker") 96 | os.Exit(1) 97 | } 98 | } 99 | 100 | func subcommandPrepare(targetOS string) { 101 | for _, mode := range prepareBuildModes { 102 | enginecache.ValidateOrUpdateEngine(targetOS, prepareCachePath, prepareEngineVersion, mode) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cmd/publish-plugin.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/go-flutter-desktop/hover/internal/build" 14 | "github.com/go-flutter-desktop/hover/internal/log" 15 | "github.com/go-flutter-desktop/hover/internal/pubspec" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | func init() { 20 | rootCmd.AddCommand(publishPluginCmd) 21 | } 22 | 23 | var publishPluginCmd = &cobra.Command{ 24 | Use: "publish-plugin", 25 | Short: "Publish your go-flutter plugin as golang module in your github repo.", 26 | Args: func(cmd *cobra.Command, args []string) error { 27 | if len(args) != 0 { 28 | return errors.New("does not take arguments") 29 | } 30 | return nil 31 | }, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | assertInFlutterPluginProject() 34 | // check if dir 'go' is tracked 35 | goCheckTrackedCmd := exec.Command(build.GitBin(), "ls-files", "--error-unmatch", build.BuildPath) 36 | goCheckTrackedCmd.Stderr = os.Stderr 37 | err := goCheckTrackedCmd.Run() 38 | if err != nil { 39 | log.Errorf("The '%s' directory doesn't seems to be tracked by git. Error: %v", build.BuildPath, err) 40 | os.Exit(1) 41 | } 42 | 43 | // check if dir 'go' is clean (all tracked files are committed) 44 | goCheckCleanCmd := exec.Command(build.GitBin(), "status", "--untracked-file=no", "--porcelain", build.BuildPath) 45 | goCheckCleanCmd.Stderr = os.Stderr 46 | cleanOut, err := goCheckCleanCmd.Output() 47 | if err != nil { 48 | log.Errorf("Failed to check if '%s' is clean.", build.BuildPath, err) 49 | os.Exit(1) 50 | } 51 | if len(cleanOut) != 0 { 52 | log.Errorf("The '%s' directory doesn't seems to be clean. (make sure tracked files are committed)", build.BuildPath) 53 | os.Exit(1) 54 | } 55 | 56 | // check if one of the git remote urls equals the package import 'url' 57 | pluginImportStr, err := readPluginGoImport(filepath.Join(build.BuildPath, "import.go.tmpl"), pubspec.GetPubSpec().Name) 58 | if err != nil { 59 | log.Errorf("Failed to read the plugin import url: %v", err) 60 | log.Infof("The file go/import.go.tmpl should look something like this:") 61 | fmt.Printf(`package main 62 | 63 | import ( 64 | flutter "github.com/go-flutter-desktop/go-flutter" 65 | %s "github.com/my-organization/%s/go" 66 | ) 67 | 68 | // .. [init function] .. 69 | `, pubspec.GetPubSpec().Name, pubspec.GetPubSpec().Name) 70 | os.Exit(1) 71 | } 72 | url, err := url.Parse("https://" + pluginImportStr) 73 | if err != nil { 74 | log.Errorf("Failed to parse %s: %v", pluginImportStr, err) 75 | os.Exit(1) 76 | } 77 | // from go import string "github.com/my-organization/test_hover/go" 78 | // check if `git remote -v` has a match on: 79 | // origin ?github.com?my-organization/test_hover.git 80 | // this regex works on https and ssh remotes. 81 | path := strings.TrimPrefix(url.Path, "/") 82 | path = strings.TrimSuffix(path, "/go") 83 | re := regexp.MustCompile(`(\w+)\s+(\S+)` + url.Host + "." + path + ".git") 84 | goCheckRemote := exec.Command(build.GitBin(), "remote", "-v") 85 | goCheckRemote.Stderr = os.Stderr 86 | remoteOut, err := goCheckRemote.Output() 87 | if err != nil { 88 | log.Errorf("Failed to get git remotes: %v", err) 89 | os.Exit(1) 90 | } 91 | match := re.FindStringSubmatch(string(remoteOut)) 92 | if len(match) < 1 { 93 | log.Warnf("At least one git remote urls must matchs the plugin golang import URL.") 94 | log.Printf("go import URL: %s", pluginImportStr) 95 | log.Printf("git remote -v:\n%s\n", string(remoteOut)) 96 | goCheckRemote.Stdout = os.Stdout 97 | //default to origin 98 | log.Warnf("Assuming origin is where the plugin code is stored") 99 | log.Printf(" This warning can occur because the git repo name dosn't match the plugin name in pubspec.yaml") 100 | match = []string{"", "origin"} 101 | } 102 | 103 | tag := "go/v" + pubspec.GetPubSpec().GetVersion() 104 | 105 | log.Infof("Your plugin at version '%s' is ready to be publish as a golang module.", pubspec.GetPubSpec().GetVersion()) 106 | log.Infof("Please run: `%s`", log.Au().Magenta("git tag "+tag)) 107 | log.Infof(" `%s`", log.Au().Magenta("git push "+match[1]+" "+tag)) 108 | 109 | log.Infof(fmt.Sprintf("Let hover run those commands? ")) 110 | if askForConfirmation() { 111 | gitTag := exec.Command(build.GitBin(), "tag", tag) 112 | gitTag.Stderr = os.Stderr 113 | gitTag.Stdout = os.Stdout 114 | err = gitTag.Run() 115 | if err != nil { 116 | log.Errorf("The git command '%s' failed. Error: %v", gitTag.String(), err) 117 | os.Exit(1) 118 | } 119 | 120 | gitPush := exec.Command(build.GitBin(), "push", match[1], tag) 121 | gitPush.Stderr = os.Stderr 122 | gitPush.Stdout = os.Stdout 123 | err = gitPush.Run() 124 | if err != nil { 125 | log.Errorf("The git command '%s' failed. Error: %v", gitPush.String(), err) 126 | os.Exit(1) 127 | } 128 | } 129 | 130 | }, 131 | } 132 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/go-flutter-desktop/hover/internal/log" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var verbose bool 13 | var colors bool 14 | var docker bool 15 | 16 | func init() { 17 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "increase logging verbosity") 18 | rootCmd.PersistentFlags().BoolVar(&colors, "colors", true, "Add colors to log") 19 | rootCmd.PersistentFlags().BoolVar(&docker, "docker", false, "Run the command in a docker container for hover") 20 | } 21 | 22 | func initHover() { 23 | log.Colorize(colors) 24 | log.Verbosity(verbose) 25 | 26 | c := make(chan os.Signal, 1) 27 | signal.Notify(c, os.Interrupt) 28 | go func() { 29 | for range c { 30 | fmt.Println("") 31 | os.Exit(1) 32 | } 33 | }() 34 | } 35 | 36 | var rootCmd = &cobra.Command{ 37 | Use: "hover", 38 | Short: "Hover connects Flutter and go-flutter-desktop.", 39 | Long: "Hover helps developers to release Flutter applications on desktop.", 40 | } 41 | 42 | // Execute executes the rootCmd 43 | func Execute() { 44 | cobra.OnInitialize(initHover) 45 | if err := rootCmd.Execute(); err != nil { 46 | log.Errorf("Command failed: %v", err) 47 | os.Exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "runtime" 11 | 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/go-flutter-desktop/hover/cmd/packaging" 15 | "github.com/go-flutter-desktop/hover/internal/build" 16 | "github.com/go-flutter-desktop/hover/internal/config" 17 | "github.com/go-flutter-desktop/hover/internal/log" 18 | "github.com/go-flutter-desktop/hover/internal/pubspec" 19 | ) 20 | 21 | var ( 22 | runObservatoryPort string 23 | runInitialRoute string 24 | ) 25 | 26 | func init() { 27 | initCompileFlags(runCmd) 28 | 29 | runCmd.Flags().StringVar(&runInitialRoute, "route", "", "Which route to load when running the app.") 30 | runCmd.Flags().StringVarP(&runObservatoryPort, "observatory-port", "", "50300", "The observatory port used to connect hover to VM services (hot-reload/debug/..)") 31 | rootCmd.AddCommand(runCmd) 32 | } 33 | 34 | var runCmd = &cobra.Command{ 35 | Use: "run", 36 | Short: "Build and start a desktop release, with hot-reload support", 37 | Run: func(cmd *cobra.Command, args []string) { 38 | projectName := pubspec.GetPubSpec().Name 39 | assertHoverInitialized() 40 | 41 | // Can only run on host OS 42 | targetOS := runtime.GOOS 43 | 44 | initBuildParameters(targetOS, build.DebugMode) 45 | subcommandBuild(targetOS, packaging.NoopTask, []string{ 46 | "--observatory-port=" + runObservatoryPort, 47 | "--enable-service-port-fallback", 48 | "--disable-service-auth-codes", 49 | }) 50 | 51 | log.Infof("Build finished, starting app...") 52 | runAndAttach(projectName, targetOS) 53 | }, 54 | } 55 | 56 | func runAndAttach(projectName string, targetOS string) { 57 | cmdApp := exec.Command(build.OutputBinaryPath(config.GetConfig().GetExecutableName(projectName), targetOS, buildOrRunMode)) 58 | cmdApp.Env = append(os.Environ(), 59 | "GOFLUTTER_ROUTE="+runInitialRoute) 60 | cmdFlutterAttach := exec.Command("flutter", "attach") 61 | 62 | stdoutApp, err := cmdApp.StdoutPipe() 63 | if err != nil { 64 | log.Errorf("Unable to create stdout pipe on app: %v", err) 65 | os.Exit(1) 66 | } 67 | stderrApp, err := cmdApp.StderrPipe() 68 | if err != nil { 69 | log.Errorf("Unable to create stderr pipe on app: %v", err) 70 | os.Exit(1) 71 | } 72 | 73 | regexObservatory := regexp.MustCompile(`listening\son\s(http:[^:]*:\d*/)`) 74 | 75 | // asynchronously read the stdout to catch the debug-uri 76 | go func(reader io.Reader) { 77 | scanner := bufio.NewScanner(reader) 78 | for scanner.Scan() { 79 | text := scanner.Text() 80 | fmt.Println(text) 81 | match := regexObservatory.FindStringSubmatch(text) 82 | if len(match) == 2 { 83 | log.Infof("Connecting hover to '%s' for hot reload", projectName) 84 | startHotReloadProcess(cmdFlutterAttach, buildOrRunFlutterTarget, match[1]) 85 | break 86 | } 87 | } 88 | // echo command Stdout to terminal 89 | io.Copy(os.Stdout, stdoutApp) 90 | }(stdoutApp) 91 | 92 | // Non-blockingly echo command stderr to terminal 93 | go io.Copy(os.Stderr, stderrApp) 94 | 95 | log.Infof("Running %s in %s mode", projectName, buildOrRunMode.Name) 96 | err = cmdApp.Start() 97 | if err != nil { 98 | log.Errorf("Failed to start app '%s': %v", projectName, err) 99 | os.Exit(1) 100 | } 101 | 102 | err = cmdApp.Wait() 103 | if err != nil { 104 | log.Errorf("App '%s' exited with error: %v", projectName, err) 105 | os.Exit(cmdApp.ProcessState.ExitCode()) 106 | } 107 | log.Infof("App '%s' exited.", projectName) 108 | log.Printf("Closing the flutter attach sub process..") 109 | cmdFlutterAttach.Wait() 110 | os.Exit(0) 111 | } 112 | 113 | func startHotReloadProcess(cmdFlutterAttach *exec.Cmd, buildTargetMainDart string, uri string) { 114 | cmdFlutterAttach.Stdin = os.Stdin 115 | cmdFlutterAttach.Stdout = os.Stdout 116 | cmdFlutterAttach.Stderr = os.Stderr 117 | 118 | cmdFlutterAttach.Args = []string{ 119 | "flutter", "attach", 120 | "--target", buildTargetMainDart, 121 | "--device-id", "flutter-tester", 122 | "--debug-uri", uri, 123 | } 124 | err := cmdFlutterAttach.Start() 125 | if err != nil { 126 | log.Warnf("The command 'flutter attach' failed: %v hot reload disabled", err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/go-flutter-desktop/hover/internal/version" 9 | ) 10 | 11 | func init() { 12 | rootCmd.AddCommand(versionCmd) 13 | } 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Print Hover version information", 18 | Args: func(cmd *cobra.Command, args []string) error { 19 | if len(args) > 0 { 20 | return errors.New("No arguments allowed") 21 | } 22 | return nil 23 | }, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | version := version.HoverVersion() 26 | fmt.Printf("Hover %s\n", version) 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /docker/hover-safe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function finish { 4 | # If we don't know what UID/GID the user outside docker has, we cannot fix 5 | # file ownership. This seems to be fine when on windows. 6 | if [[ ${HOVER_SAFE_CHOWN_UID} && ${HOVER_SAFE_CHOWN_UID-x} ]]; then 7 | chown -R ${HOVER_SAFE_CHOWN_UID}:${HOVER_SAFE_CHOWN_GID} /app 8 | chown -R ${HOVER_SAFE_CHOWN_UID}:${HOVER_SAFE_CHOWN_GID} /root/.cache/hover 9 | chown -R ${HOVER_SAFE_CHOWN_UID}:${HOVER_SAFE_CHOWN_GID} /go-cache 10 | # echo "chowned files to ${HOVER_SAFE_CHOWN_UID}:${HOVER_SAFE_CHOWN_GID}" 11 | fi 12 | } 13 | trap finish EXIT 14 | 15 | hover $@ 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-flutter-desktop/hover 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/JackMordaunt/icns v1.0.0 9 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 10 | github.com/google/uuid v1.6.0 11 | github.com/hashicorp/go-version v1.7.0 12 | github.com/logrusorgru/aurora v2.0.3+incompatible 13 | github.com/otiai10/copy v1.14.1 14 | github.com/pkg/errors v0.9.1 15 | github.com/spf13/cobra v1.9.1 16 | github.com/stretchr/testify v1.10.0 17 | github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e 18 | golang.org/x/mod v0.25.0 19 | golang.org/x/sys v0.24.0 20 | gopkg.in/yaml.v2 v2.4.0 21 | ) 22 | 23 | require ( 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/google/go-github v17.0.0+incompatible // indirect 26 | github.com/google/go-querystring v1.1.0 // indirect 27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 28 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 29 | github.com/otiai10/mint v1.6.3 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/spf13/pflag v1.0.6 // indirect 32 | golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect 33 | golang.org/x/sync v0.8.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/JackMordaunt/icns v1.0.0 h1:41cNyWyQrG6beMw7m93LFK5o1GhefflsBTkauUkUtG8= 2 | github.com/JackMordaunt/icns v1.0.0/go.mod h1:ubRqphS0f2OD07BuNaQSuw9uHUVQNBX5g38n6i2bdqM= 3 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 h1:1ltqoej5GtaWF8jaiA49HwsZD459jqm9YFz9ZtMFpQA= 4 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 9 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 11 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 12 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 13 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 17 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 18 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 19 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 20 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 21 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 22 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 23 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 24 | github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= 25 | github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= 26 | github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= 27 | github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= 28 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 29 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 33 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 34 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 35 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 36 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 37 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 38 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k= 40 | github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= 41 | golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= 42 | golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 43 | golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= 44 | golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 45 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 46 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 47 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 48 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 49 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 53 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 54 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 55 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 56 | -------------------------------------------------------------------------------- /install-with-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | go install . && DOCKER_BUILDKIT=1 docker build . -t goflutter/hover:latest 3 | -------------------------------------------------------------------------------- /internal/androidmanifest/android-manifest.go: -------------------------------------------------------------------------------- 1 | package androidmanifest 2 | 3 | import ( 4 | "encoding/xml" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/go-flutter-desktop/hover/internal/log" 10 | ) 11 | 12 | // AndroidManifest is a file that describes the essential information about 13 | // an android app. 14 | type AndroidManifest struct { 15 | Package string `xml:"package,attr"` 16 | } 17 | 18 | // AndroidOrganizationName fetch the android package name (default: 19 | // 'com.example'). 20 | // Can by set upon flutter create (--org flag) 21 | // 22 | // If errors occurs when reading the android package name, the string value 23 | // will correspond to 'hover.failed.to.retrieve.package.name' 24 | func AndroidOrganizationName() string { 25 | // Default value 26 | androidManifestFile := "android/app/src/main/AndroidManifest.xml" 27 | 28 | // Open AndroidManifest file 29 | xmlFile, err := os.Open(androidManifestFile) 30 | if err != nil { 31 | log.Errorf("Failed to retrieve the organization name: %v", err) 32 | return "hover.failed.to.retrieve.package.name" 33 | } 34 | defer xmlFile.Close() 35 | 36 | byteXMLValue, err := ioutil.ReadAll(xmlFile) 37 | if err != nil { 38 | log.Errorf("Failed to retrieve the organization name: %v", err) 39 | return "hover.failed.to.retrieve.package.name" 40 | } 41 | 42 | var androidManifest AndroidManifest 43 | err = xml.Unmarshal(byteXMLValue, &androidManifest) 44 | if err != nil { 45 | log.Errorf("Failed to retrieve the organization name: %v", err) 46 | return "hover.failed.to.retrieve.package.name" 47 | } 48 | javaPackage := strings.Split(androidManifest.Package, ".") 49 | if len(javaPackage) > 2 { 50 | javaPackage = javaPackage[:len(javaPackage)-1] 51 | } 52 | orgName := strings.Join(javaPackage, ".") 53 | if orgName == "" { 54 | return "hover.failed.to.retrieve.package.name" 55 | } 56 | return orgName 57 | } 58 | -------------------------------------------------------------------------------- /internal/build/binaries.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "sync" 7 | 8 | "github.com/go-flutter-desktop/hover/internal/log" 9 | ) 10 | 11 | type binLookup struct { 12 | Name string 13 | InstallInstructions string 14 | fullPath string 15 | once sync.Once 16 | } 17 | 18 | func (b *binLookup) FullPath() string { 19 | b.once.Do(func() { 20 | var err error 21 | b.fullPath, err = exec.LookPath(b.Name) 22 | if err != nil { 23 | log.Errorf("Failed to lookup `%s` executable: %s. %s", b.Name, err, b.InstallInstructions) 24 | os.Exit(1) 25 | } 26 | }) 27 | return b.fullPath 28 | } 29 | 30 | var ( 31 | goBinLookup = binLookup{ 32 | Name: "go", 33 | InstallInstructions: "Please install go or add `--docker` to run the Hover command in a Docker container.\nhttps://golang.org/doc/install", 34 | } 35 | flutterBinLookup = binLookup{ 36 | Name: "flutter", 37 | InstallInstructions: "Please install flutter or add `--docker` to run the Hover command in Docker container.\nhttps://flutter.dev/docs/get-started/install", 38 | } 39 | gitBinLookup = binLookup{ 40 | Name: "git", 41 | } 42 | dockerBinLookup = binLookup{ 43 | Name: "docker", 44 | } 45 | ) 46 | 47 | func GoBin() string { 48 | return goBinLookup.FullPath() 49 | } 50 | 51 | func FlutterBin() string { 52 | return flutterBinLookup.FullPath() 53 | } 54 | 55 | func GitBin() string { 56 | return gitBinLookup.FullPath() 57 | } 58 | 59 | func DockerBin() string { 60 | return dockerBinLookup.FullPath() 61 | } 62 | -------------------------------------------------------------------------------- /internal/build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-flutter-desktop/hover/internal/log" 9 | ) 10 | 11 | // BuildPath sets the name of the directory used to store the go-flutter project. 12 | // Much like android and ios are already used. 13 | const BuildPath = "go" 14 | 15 | // buildDirectoryPath returns the path in `BuildPath`/build. 16 | // If needed, the directory is create at the returned path. 17 | func buildDirectoryPath(targetOS string, mode Mode, path string) string { 18 | outputDirectoryPath, err := filepath.Abs(filepath.Join(BuildPath, "build", path, fmt.Sprintf("%s-%s", targetOS, mode.Name))) 19 | if err != nil { 20 | log.Errorf("Failed to resolve absolute path for output directory: %v", err) 21 | os.Exit(1) 22 | } 23 | if _, err := os.Stat(outputDirectoryPath); os.IsNotExist(err) { 24 | err = os.MkdirAll(outputDirectoryPath, 0775) 25 | if err != nil { 26 | log.Errorf("Failed to create output directory %s: %v", outputDirectoryPath, err) 27 | os.Exit(1) 28 | } 29 | } 30 | return outputDirectoryPath 31 | } 32 | 33 | // OutputDirectoryPath returns the path where the go-flutter binary and flutter 34 | // binaries blobs will be stored for a particular platform. 35 | // If needed, the directory is create at the returned path. 36 | func OutputDirectoryPath(targetOS string, mode Mode) string { 37 | return buildDirectoryPath(targetOS, mode, "outputs") 38 | } 39 | 40 | // IntermediatesDirectoryPath returns the path where the intermediates stored. 41 | // If needed, the directory is create at the returned path. 42 | // 43 | // Those intermediates include the dynamic library dependencies of go-flutter plugins. 44 | // hover copies these intermediates from flutter plugins folder when `hover plugins get`, and 45 | // copies to go-flutter's binary output folder before build. 46 | func IntermediatesDirectoryPath(targetOS string, mode Mode) string { 47 | return buildDirectoryPath(targetOS, mode, "intermediates") 48 | } 49 | 50 | // OutputBinary returns the string of the executable used to launch the 51 | // main desktop app. (appends .exe for windows) 52 | func OutputBinary(executableName, targetOS string) string { 53 | var outputBinaryName = executableName 54 | switch targetOS { 55 | case "darwin": 56 | // no special filename 57 | case "linux": 58 | // no special filename 59 | case "windows": 60 | outputBinaryName += ".exe" 61 | default: 62 | log.Errorf("Target platform %s is not supported.", targetOS) 63 | os.Exit(1) 64 | } 65 | return outputBinaryName 66 | } 67 | 68 | // OutputBinaryPath returns the path to the go-flutter Application for a 69 | // specified platform. 70 | func OutputBinaryPath(executableName, targetOS string, mode Mode) string { 71 | outputBinaryPath := filepath.Join(OutputDirectoryPath(targetOS, mode), OutputBinary(executableName, targetOS)) 72 | return outputBinaryPath 73 | } 74 | 75 | // ExecutableExtension returns the extension of binary files on a given platform 76 | func ExecutableExtension(targetOS string) string { 77 | switch targetOS { 78 | case "darwin": 79 | // no special filename 80 | return "" 81 | case "linux": 82 | // no special filename 83 | return "" 84 | case "windows": 85 | return ".exe" 86 | default: 87 | log.Errorf("Target platform %s is not supported.", targetOS) 88 | os.Exit(1) 89 | return "" 90 | } 91 | } 92 | 93 | // EngineFiles returns the names of the engine files from flutter for the 94 | // specified platform and build mode. 95 | func EngineFiles(targetOS string, mode Mode) []string { 96 | switch targetOS { 97 | case "darwin": 98 | if mode.IsAot { 99 | return []string{"libflutter_engine.dylib"} 100 | } else { 101 | return []string{"FlutterEmbedder.framework"} 102 | } 103 | case "linux": 104 | return []string{"libflutter_engine.so"} 105 | case "windows": 106 | if mode.IsAot { 107 | return []string{"flutter_engine.dll", "flutter_engine.exp", "flutter_engine.lib", "flutter_engine.pdb"} 108 | } else { 109 | return []string{"flutter_engine.dll"} 110 | } 111 | default: 112 | log.Errorf("%s has no implemented engine file", targetOS) 113 | os.Exit(1) 114 | return []string{} 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /internal/build/mode.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | type Mode struct { 4 | Name string 5 | IsAot bool 6 | } 7 | 8 | var DebugMode = Mode{ 9 | Name: "debug_unopt", 10 | IsAot: false, 11 | } 12 | 13 | // JitReleaseMode is the same debug build, but disables the terminal windows on Windows 14 | var JitReleaseMode = Mode{ 15 | Name: "debug_unopt", 16 | IsAot: false, 17 | } 18 | 19 | var ReleaseMode = Mode{ 20 | Name: "release", 21 | IsAot: true, 22 | } 23 | 24 | var ProfileMode = Mode{ 25 | Name: "profile", 26 | IsAot: true, 27 | } 28 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/pkg/errors" 10 | "gopkg.in/yaml.v2" 11 | 12 | "github.com/go-flutter-desktop/hover/internal/androidmanifest" 13 | "github.com/go-flutter-desktop/hover/internal/build" 14 | "github.com/go-flutter-desktop/hover/internal/log" 15 | ) 16 | 17 | // BuildTargetDefault Default build target file 18 | const BuildTargetDefault = "lib/main_desktop.dart" 19 | 20 | // BuildEngineDefault Default go-flutter engine version 21 | const BuildEngineDefault = "" 22 | 23 | // BuildOpenGlVersionDefault Default OpenGL version for go-flutter 24 | const BuildOpenGlVersionDefault = "3.3" 25 | 26 | // Config contains the parsed contents of hover.yaml 27 | type Config struct { 28 | ApplicationName string `yaml:"application-name"` 29 | ExecutableName string `yaml:"executable-name"` 30 | PackageName string `yaml:"package-name"` 31 | OrganizationName string `yaml:"organization-name"` 32 | License string 33 | Target string 34 | BranchREMOVED string `yaml:"branch"` 35 | CachePathREMOVED string `yaml:"cache-path"` 36 | OpenGL string 37 | Engine string `yaml:"engine-version"` 38 | } 39 | 40 | func (c Config) GetApplicationName(projectName string) string { 41 | if c.ApplicationName == "" { 42 | return projectName 43 | } 44 | return c.ApplicationName 45 | } 46 | 47 | func (c Config) GetExecutableName(projectName string) string { 48 | if c.ExecutableName == "" { 49 | return strings.ReplaceAll(projectName, " ", "") 50 | } 51 | return c.ExecutableName 52 | } 53 | 54 | func (c Config) GetPackageName(projectName string) string { 55 | if c.PackageName == "" { 56 | return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(projectName, "-", ""), "_", ""), " ", "") 57 | } 58 | return c.PackageName 59 | } 60 | 61 | func (c Config) GetOrganizationName() string { 62 | if len(c.OrganizationName) == 0 { 63 | PrintMissingField("organization-name", "go/hover.yaml", c.OrganizationName) 64 | // It would be nicer to not load a value from the AndroidManifest.xml and instead define a default value here, 65 | // but then older apps might break so for compatibility reasons it's done this way. 66 | c.OrganizationName = androidmanifest.AndroidOrganizationName() 67 | } 68 | return c.OrganizationName 69 | } 70 | 71 | func (c Config) GetLicense() string { 72 | if len(c.License) == 0 { 73 | c.License = "NOASSERTION" 74 | PrintMissingField("license", "go/hover.yaml", c.License) 75 | } 76 | return c.License 77 | } 78 | 79 | var ( 80 | config Config 81 | configLoadOnce sync.Once 82 | ) 83 | 84 | // GetConfig returns the working directory hover.yaml as a Config 85 | func GetConfig() Config { 86 | configLoadOnce.Do(func() { 87 | var err error 88 | hoverYaml := GetHoverFlavorYaml() 89 | config, err = ReadConfigFile(filepath.Join(build.BuildPath, hoverYaml)) 90 | if err != nil { 91 | if os.IsNotExist(errors.Cause(err)) { 92 | // TODO: Add a solution for the user. Perhaps we can let `hover 93 | // init` write missing files when ran on an existing project. 94 | // https://github.com/go-flutter-desktop/hover/pull/121#pullrequestreview-408680348 95 | log.Warnf("Missing config: %v", err) 96 | return 97 | } 98 | log.Errorf("Failed to load config: %v", err) 99 | os.Exit(1) 100 | } 101 | 102 | if config.CachePathREMOVED != "" { 103 | log.Errorf("The hover.yaml field 'cache-path' is not used anymore. Remove it from your hover.yaml and use --cache-path instead.") 104 | os.Exit(1) 105 | } 106 | if config.BranchREMOVED != "" { 107 | log.Errorf("The hover.yaml field 'branch' is not used anymore. Remove it from your hover.yaml and use --branch instead.") 108 | os.Exit(1) 109 | } 110 | }) 111 | return config 112 | } 113 | 114 | // ReadConfigFile reads a .yaml file at a path and return a correspond Config 115 | // struct 116 | func ReadConfigFile(configPath string) (Config, error) { 117 | file, err := os.Open(configPath) 118 | if err != nil { 119 | if os.IsNotExist(err) { 120 | return Config{}, errors.Wrap(err, "file hover.yaml not found") 121 | } 122 | return Config{}, errors.Wrap(err, "failed to open hover.yaml") 123 | } 124 | defer file.Close() 125 | 126 | var config Config 127 | err = yaml.NewDecoder(file).Decode(&config) 128 | if err != nil { 129 | return Config{}, errors.Wrap(err, "failed to decode hover.yaml") 130 | } 131 | return config, nil 132 | } 133 | 134 | func PrintMissingField(name, file, def string) { 135 | log.Warnf("Missing/Empty `%s` field in %s. Please add it or otherwise you may publish your app with a wrong %s. Continuing with `%s` as a placeholder %s.", name, file, name, def, name) 136 | } 137 | -------------------------------------------------------------------------------- /internal/config/flavor.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-flutter-desktop/hover/internal/build" 9 | "github.com/go-flutter-desktop/hover/internal/log" 10 | ) 11 | 12 | var hoverYaml string 13 | 14 | // GetHoverFlavorYaml returns the Hover yaml file 15 | func GetHoverFlavorYaml() string { 16 | if len(hoverYaml) == 0 { 17 | hoverYaml = "hover.yaml" 18 | } 19 | return hoverYaml 20 | } 21 | 22 | // SetHoverFlavor sets the user defined hover flavor. 23 | // eg. hover-develop.yaml, hover-staging.yaml, etc. 24 | func SetHoverFlavor(flavor string) { 25 | hoverYaml = fmt.Sprintf("hover-%s.yaml", flavor) 26 | assertYamlFileExists(hoverYaml) 27 | } 28 | 29 | // assertYamlFileExists checks to see if the user defined yaml file exists 30 | func assertYamlFileExists(yamlFile string) { 31 | _, err := os.Stat(filepath.Join(build.BuildPath, yamlFile)) 32 | if os.IsNotExist(err) { 33 | log.Warnf("Hover Yaml file \"%s\" not found.", yamlFile) 34 | os.Exit(1) 35 | } 36 | if err != nil { 37 | log.Errorf("Failed to stat %s: %v\n", yamlFile, err) 38 | os.Exit(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/darwinhacks/darwinhacks.go: -------------------------------------------------------------------------------- 1 | package darwinhacks 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/go-flutter-desktop/hover/internal/log" 13 | ) 14 | 15 | // DyldHack is a nasty hack to get the linking working. After fiddling a lot of hours with CGO linking 16 | // this was the only solution I could come up with and it works. I guess something would need to be changed in the engine 17 | // builds to make this obsolete, but this hack does it for now. 18 | func DyldHack(path string) { 19 | installNameToolCommand := []string{ 20 | "install_name_tool", 21 | "-change", 22 | "./libflutter_engine.dylib", 23 | "@executable_path/libflutter_engine.dylib", 24 | "-id", 25 | "@executable_path/libflutter_engine.dylib", 26 | RewriteDarlingPath(runtime.GOOS != "darwin", path), 27 | } 28 | if runtime.GOOS != "darwin" { 29 | installNameToolCommand = append([]string{"darling", "shell"}, installNameToolCommand...) 30 | } 31 | cmdInstallNameTool := exec.Command( 32 | installNameToolCommand[0], 33 | installNameToolCommand[1:]..., 34 | ) 35 | cmdInstallNameTool.Stderr = os.Stderr 36 | output, err := cmdInstallNameTool.Output() 37 | if err != nil { 38 | log.Errorf("install_name_tool failed: %v", err) 39 | log.Errorf(string(output)) 40 | os.Exit(1) 41 | } 42 | } 43 | 44 | func RewriteDarlingPath(useDarling bool, path string) string { 45 | if useDarling { 46 | return filepath.Join("/", "Volumes", "SystemRoot", path) 47 | } 48 | return path 49 | } 50 | 51 | func ChangePackagesFilePath(isInsert bool) { 52 | for _, path := range []string{".packages", filepath.Join(".dart_tool", "package_config.json")} { 53 | content, err := ioutil.ReadFile(path) 54 | if err != nil { 55 | log.Errorf("Failed to read %s file: %v", path, err) 56 | os.Exit(1) 57 | } 58 | lines := strings.Split(string(content), "\n") 59 | for i := range lines { 60 | if strings.Contains(lines[i], "file://") { 61 | parts := strings.Split(lines[i], "file://") 62 | if isInsert && !strings.Contains(lines[i], "/Volumes/SystemRoot") { 63 | lines[i] = fmt.Sprintf("%sfile:///Volumes/SystemRoot%s", parts[0], parts[1]) 64 | } else { 65 | lines[i] = fmt.Sprintf("%sfile://%s", parts[0], strings.ReplaceAll(parts[1], "/Volumes/SystemRoot", "")) 66 | } 67 | } 68 | } 69 | err = ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644) 70 | if err != nil { 71 | log.Errorf("Failed to write %s file: %v", path, err) 72 | os.Exit(1) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/enginecache/cache.go: -------------------------------------------------------------------------------- 1 | package enginecache 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/otiai10/copy" 18 | "github.com/pkg/errors" 19 | 20 | "github.com/go-flutter-desktop/hover/internal/build" 21 | "github.com/go-flutter-desktop/hover/internal/darwinhacks" 22 | "github.com/go-flutter-desktop/hover/internal/log" 23 | "github.com/go-flutter-desktop/hover/internal/version" 24 | ) 25 | 26 | func createSymLink(oldname, newname string) error { 27 | err := os.Remove(newname) 28 | if err != nil && !os.IsNotExist(err) { 29 | return errors.Wrap(err, "failed to remove existing symlink") 30 | } 31 | 32 | err = os.Symlink(oldname, newname) 33 | if err != nil { 34 | return errors.Wrap(err, "failed to create symlink") 35 | } 36 | return nil 37 | } 38 | 39 | // Unzip will decompress a zip archive, moving all files and folders 40 | // within the zip file (parameter 1) to an output directory (parameter 2). 41 | func unzip(src string, dest string) ([]string, error) { 42 | var filenames []string 43 | 44 | r, err := zip.OpenReader(src) 45 | if err != nil { 46 | return filenames, err 47 | } 48 | defer r.Close() 49 | 50 | for _, f := range r.File { 51 | 52 | // Store filename/path for returning and using later on 53 | fpath := filepath.Join(dest, f.Name) 54 | 55 | // Check for ZipSlip. More Infof: http://bit.ly/2MsjAWE 56 | if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { 57 | return filenames, fmt.Errorf("%s: illegal file path", fpath) 58 | } 59 | 60 | filenames = append(filenames, fpath) 61 | 62 | if f.FileInfo().IsDir() { 63 | // Make Folder 64 | os.MkdirAll(fpath, os.ModePerm) 65 | continue 66 | } 67 | 68 | // Make File 69 | if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { 70 | return filenames, err 71 | } 72 | 73 | outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 74 | if err != nil { 75 | return filenames, err 76 | } 77 | 78 | rc, err := f.Open() 79 | if err != nil { 80 | return filenames, err 81 | } 82 | 83 | _, err = io.Copy(outFile, rc) 84 | 85 | // Close the file without defer to close before next iteration of loop 86 | outFile.Close() 87 | rc.Close() 88 | 89 | if err != nil { 90 | return filenames, err 91 | } 92 | } 93 | return filenames, nil 94 | } 95 | 96 | // Function to prind download percent completion 97 | func printDownloadPercent(done chan chan struct{}, path string, expectedSize int64) { 98 | var completedCh chan struct{} 99 | for { 100 | fi, err := os.Stat(path) 101 | if err != nil { 102 | log.Warnf("%v", err) 103 | } 104 | 105 | size := fi.Size() 106 | 107 | if size == 0 { 108 | size = 1 109 | } 110 | 111 | var percent = float64(size) / float64(expectedSize) * 100 112 | 113 | // We use '\033[2K\r' to avoid carriage return, it will print above previous. 114 | fmt.Printf("\033[2K\r %.0f %% / 100 %%", percent) 115 | 116 | if completedCh != nil { 117 | close(completedCh) 118 | return 119 | } 120 | 121 | select { 122 | case completedCh = <-done: 123 | case <-time.After(time.Second / 60): // Flutter promises 60fps, right? ;) 124 | } 125 | } 126 | } 127 | 128 | // Function to download file with given path and url. 129 | func downloadFile(filepath string, url string) error { 130 | // // Printf download url in case user needs it. 131 | // log.Printf("Downloading file from\n '%s'\n to '%s'", url, filepath) 132 | 133 | start := time.Now() 134 | 135 | // Create the file 136 | out, err := os.Create(filepath) 137 | if err != nil { 138 | return err 139 | } 140 | defer out.Close() 141 | 142 | // Get the data 143 | resp, err := http.Get(url) 144 | if err != nil { 145 | return err 146 | } 147 | defer resp.Body.Close() 148 | 149 | expectedSize, err := strconv.Atoi(resp.Header.Get("Content-Length")) 150 | if err != nil { 151 | return errors.Wrap(err, "failed to get Content-Length header") 152 | } 153 | 154 | doneCh := make(chan chan struct{}) 155 | go printDownloadPercent(doneCh, filepath, int64(expectedSize)) 156 | 157 | _, err = io.Copy(out, resp.Body) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | // close channel to indicate we're done 163 | doneCompletedCh := make(chan struct{}) 164 | doneCh <- doneCompletedCh // signal that download is done 165 | <-doneCompletedCh // wait for signal that printing has completed 166 | 167 | elapsed := time.Since(start) 168 | log.Printf("\033[2K\rDownload completed in %.2fs", elapsed.Seconds()) 169 | return nil 170 | } 171 | 172 | func EngineConfig(targetOS string, mode build.Mode) string { 173 | return fmt.Sprintf("%s-%s", targetOS, mode.Name) 174 | } 175 | 176 | //noinspection GoNameStartsWithPackageName 177 | func EngineCachePath(targetOS, cachePath string, mode build.Mode) string { 178 | return filepath.Join(BaseEngineCachePath(cachePath), EngineConfig(targetOS, mode)) 179 | } 180 | 181 | func BaseEngineCachePath(cachePath string) string { 182 | return filepath.Join(cachePath, "hover", "engine") 183 | } 184 | 185 | // ValidateOrUpdateEngine validates the engine we have cached matches the 186 | // flutter version, or otherwise downloads a new engine. The engine cache 187 | // location is set by the the user. 188 | func ValidateOrUpdateEngine(targetOS, cachePath, requiredEngineVersion string, mode build.Mode) { 189 | engineCachePath := EngineCachePath(targetOS, cachePath, mode) 190 | 191 | if strings.Contains(engineCachePath, " ") { 192 | log.Errorf("Cannot save the engine to '%s', engine cache is not compatible with path containing spaces.", cachePath) 193 | log.Errorf(" Please run hover with a another engine cache path. Example:") 194 | log.Errorf(" %s", log.Au().Magenta("hover run --cache-path \"C:\\cache\"")) 195 | log.Errorf(" The --cache-path flag will have to be provided to every build and run command.") 196 | os.Exit(1) 197 | } 198 | 199 | cachedEngineVersionPath := filepath.Join(engineCachePath, "version") 200 | cachedEngineVersionBytes, err := ioutil.ReadFile(cachedEngineVersionPath) 201 | if err != nil && !os.IsNotExist(err) { 202 | log.Errorf("Failed to read cached engine version: %v", err) 203 | os.Exit(1) 204 | } 205 | cachedEngineVersion := string(cachedEngineVersionBytes) 206 | if len(requiredEngineVersion) == 0 { 207 | requiredEngineVersion = version.FlutterRequiredEngineVersion() 208 | } 209 | 210 | if cachedEngineVersion == fmt.Sprintf("%s-%s", requiredEngineVersion, version.HoverVersion()) { 211 | log.Printf("Using engine from cache") 212 | return 213 | } else { 214 | // Engine is outdated, we remove the old engine and continue to download 215 | // the new engine. 216 | err = os.RemoveAll(engineCachePath) 217 | if err != nil { 218 | log.Errorf("Failed to remove outdated engine: %v", err) 219 | os.Exit(1) 220 | } 221 | } 222 | 223 | err = os.MkdirAll(engineCachePath, 0775) 224 | if err != nil { 225 | log.Errorf("Failed to create engine cache directory: %v", err) 226 | os.Exit(1) 227 | } 228 | 229 | dir, err := ioutil.TempDir("", "hover-engine-download") 230 | if err != nil { 231 | log.Errorf("Failed to create tmp dir for engine download: %v", err) 232 | os.Exit(1) 233 | } 234 | defer os.RemoveAll(dir) 235 | 236 | err = os.MkdirAll(dir, 0700) 237 | if err != nil { 238 | log.Warnf("%v", err) 239 | } 240 | 241 | engineZipPath := filepath.Join(dir, "engine.zip") 242 | engineExtractPath := filepath.Join(dir, "engine") 243 | 244 | log.Printf("Downloading engine for platform %s at version %s...", EngineConfig(targetOS, mode), requiredEngineVersion) 245 | 246 | if mode == build.DebugMode { 247 | targetedDomain := "https://storage.googleapis.com" 248 | envURLFlutter := os.Getenv("FLUTTER_STORAGE_BASE_URL") 249 | if envURLFlutter != "" { 250 | targetedDomain = envURLFlutter 251 | } 252 | var engineDownloadURL = fmt.Sprintf(targetedDomain+"/flutter_infra_release/flutter/%s/%s-x64/", requiredEngineVersion, targetOS) 253 | switch targetOS { 254 | case "darwin": 255 | engineDownloadURL += "FlutterEmbedder.framework.zip" 256 | case "linux": 257 | engineDownloadURL += targetOS + "-x64-embedder" 258 | case "windows": 259 | engineDownloadURL += targetOS + "-x64-embedder.zip" 260 | default: 261 | log.Errorf("Cannot run on %s, download engine not implemented.", targetOS) 262 | os.Exit(1) 263 | } 264 | 265 | artifactsZipPath := filepath.Join(dir, "artifacts.zip") 266 | artifactsDownloadURL := fmt.Sprintf(targetedDomain+"/flutter_infra_release/flutter/%s/%s-x64/artifacts.zip", requiredEngineVersion, targetOS) 267 | 268 | err = downloadFile(engineZipPath, engineDownloadURL) 269 | if err != nil { 270 | log.Errorf("Failed to download engine: %v", err) 271 | os.Exit(1) 272 | } 273 | _, err = unzip(engineZipPath, engineExtractPath) 274 | if err != nil { 275 | log.Warnf("%v", err) 276 | } 277 | 278 | err = downloadFile(artifactsZipPath, artifactsDownloadURL) 279 | if err != nil { 280 | log.Errorf("Failed to download artifacts: %v", err) 281 | os.Exit(1) 282 | } 283 | _, err = unzip(artifactsZipPath, engineExtractPath) 284 | if err != nil { 285 | log.Warnf("%v", err) 286 | } 287 | if targetOS == "darwin" { 288 | frameworkZipPath := filepath.Join(engineExtractPath, "FlutterEmbedder.framework.zip") 289 | frameworkDestPath := filepath.Join(engineExtractPath, "FlutterEmbedder.framework") 290 | _, err = unzip(frameworkZipPath, frameworkDestPath) 291 | if err != nil { 292 | log.Errorf("Failed to unzip engine framework: %v", err) 293 | os.Exit(1) 294 | } 295 | createSymLink("A", frameworkDestPath+"/Versions/Current") 296 | createSymLink("Versions/Current/FlutterEmbedder", frameworkDestPath+"/FlutterEmbedder") 297 | createSymLink("Versions/Current/Headers", frameworkDestPath+"/Headers") 298 | createSymLink("Versions/Current/Modules", frameworkDestPath+"/Modules") 299 | createSymLink("Versions/Current/Resources", frameworkDestPath+"/Resources") 300 | } 301 | } else { 302 | file := "" 303 | switch targetOS { 304 | case "linux": 305 | file += "linux" 306 | case "darwin": 307 | file += "macosx" 308 | case "windows": 309 | file += "windows" 310 | } 311 | file += fmt.Sprintf("_x64-host_%s.zip", mode.Name) 312 | engineDownloadURL := fmt.Sprintf("https://github.com/go-flutter-desktop/engine-builds/releases/download/f-%s/%s", requiredEngineVersion, file) 313 | 314 | err = downloadFile(engineZipPath, engineDownloadURL) 315 | if err != nil { 316 | log.Errorf("Failed to download engine: %v", err) 317 | log.Errorf("Engine builds are a bit delayed after they are published in flutter.") 318 | log.Errorf("You can either try again later or switch the flutter channel to beta, because these engines are more likely to be already built.") 319 | log.Errorf("To dig into the already built engines look at https://github.com/go-flutter-desktop/engine-builds/releases and https://github.com/go-flutter-desktop/engine-builds/actions") 320 | os.Exit(1) 321 | } 322 | _, err = unzip(engineZipPath, engineExtractPath) 323 | if err != nil { 324 | log.Warnf("%v", err) 325 | } 326 | 327 | } 328 | 329 | for _, engineFile := range build.EngineFiles(targetOS, mode) { 330 | err := copy.Copy( 331 | filepath.Join(engineExtractPath, engineFile), 332 | filepath.Join(engineCachePath, engineFile), 333 | ) 334 | if err != nil { 335 | log.Errorf("Failed to copy downloaded %s: %v", engineFile, err) 336 | os.Exit(1) 337 | } 338 | } 339 | 340 | // Strip linux engine after download and not at every build 341 | if targetOS == "linux" && targetOS == runtime.GOOS { 342 | unstrippedEngineFile := filepath.Join(engineCachePath, build.EngineFiles(targetOS, mode)[0]) 343 | err = exec.Command("strip", "-s", unstrippedEngineFile).Run() 344 | if err != nil { 345 | log.Errorf("Failed to strip %s: %v", unstrippedEngineFile, err) 346 | os.Exit(1) 347 | } 348 | } 349 | 350 | if targetOS == "darwin" && mode != build.DebugMode { 351 | darwinhacks.DyldHack(filepath.Join(engineCachePath, build.EngineFiles(targetOS, mode)[0])) 352 | } 353 | 354 | files := []string{ 355 | "icudtl.dat", 356 | } 357 | if mode != build.DebugMode { 358 | files = append( 359 | files, 360 | "dart"+build.ExecutableExtension(targetOS), 361 | "gen_snapshot"+build.ExecutableExtension(targetOS), 362 | "gen", 363 | "flutter_patched_sdk", 364 | ) 365 | } 366 | for _, file := range files { 367 | err = copy.Copy( 368 | filepath.Join(engineExtractPath, file), 369 | filepath.Join(engineCachePath, file), 370 | ) 371 | if err != nil { 372 | log.Errorf("Failed to copy downloaded %s: %v", file, err) 373 | os.Exit(1) 374 | } 375 | } 376 | 377 | err = ioutil.WriteFile(cachedEngineVersionPath, []byte(fmt.Sprintf("%s-%s", requiredEngineVersion, version.HoverVersion())), 0664) 378 | if err != nil { 379 | log.Errorf("Failed to write version file: %v", err) 380 | os.Exit(1) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /internal/enginecache/path.go: -------------------------------------------------------------------------------- 1 | package enginecache 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-flutter-desktop/hover/internal/log" 7 | ) 8 | 9 | // DefaultCachePath tries to resolve the user cache directory. DefaultCachePath 10 | // may return an empty string when none was found, in that case it will print a 11 | // warning to the user. 12 | func DefaultCachePath() string { 13 | cachePath, err := os.UserCacheDir() 14 | if err != nil { 15 | log.Warnf("Failed to resolve cache path: %v", err) 16 | } 17 | return cachePath 18 | } 19 | -------------------------------------------------------------------------------- /internal/fileutils/assets/app/gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .last_goflutter_check 3 | -------------------------------------------------------------------------------- /internal/fileutils/assets/app/hover.yaml.tmpl: -------------------------------------------------------------------------------- 1 | #application-name: "{{.applicationName}}" # Uncomment to modify this value. 2 | #executable-name: "{{.executableName}}" # Uncomment to modify this value. Only lowercase a-z, numbers, underscores and no spaces 3 | #package-name: "{{.packageName}}" # Uncomment to modify this value. Only lowercase a-z, numbers and no underscores or spaces 4 | organization-name: "com.{{.packageName}}" 5 | license: "" # MANDATORY: Fill in your SPDX license name: https://spdx.org/licenses 6 | target: lib/main_desktop.dart 7 | # opengl: "none" # Uncomment this line if you have trouble with your OpenGL driver (https://github.com/go-flutter-desktop/go-flutter/issues/272) 8 | docker: false 9 | engine-version: "" # change to a engine version commit 10 | -------------------------------------------------------------------------------- /internal/fileutils/assets/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-flutter-desktop/hover/5ec2db61223ba00ce6d77ed5c67e6b8bbdc1cd35/internal/fileutils/assets/app/icon.png -------------------------------------------------------------------------------- /internal/fileutils/assets/app/main.go.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | _ "image/png" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/go-flutter-desktop/go-flutter" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // vmArguments may be set by hover at compile-time 16 | var vmArguments string 17 | 18 | func main() { 19 | // DO NOT EDIT, add options in options.go 20 | mainOptions := []flutter.Option{ 21 | flutter.OptionVMArguments(strings.Split(vmArguments, ";")), 22 | flutter.WindowIcon(iconProvider), 23 | } 24 | err := flutter.Run(append(options, mainOptions...)...) 25 | if err != nil { 26 | fmt.Println(err) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | func iconProvider() ([]image.Image, error) { 32 | execPath, err := os.Executable() 33 | if err != nil { 34 | return nil, errors.Wrap(err, "failed to resolve executable path") 35 | } 36 | execPath, err = filepath.EvalSymlinks(execPath) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "failed to eval symlinks for executable path") 39 | } 40 | imgFile, err := os.Open(filepath.Join(filepath.Dir(execPath), "assets", "icon.png")) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "failed to open assets/icon.png") 43 | } 44 | img, _, err := image.Decode(imgFile) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "failed to decode image") 47 | } 48 | return []image.Image{img}, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/fileutils/assets/app/main_desktop.dart: -------------------------------------------------------------------------------- 1 | import 'main.dart' as original_main; 2 | 3 | // This file is the default main entry-point for go-flutter application. 4 | void main() { 5 | original_main.main(); 6 | } 7 | -------------------------------------------------------------------------------- /internal/fileutils/assets/app/options.go.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-flutter-desktop/go-flutter" 5 | ) 6 | 7 | var options = []flutter.Option{ 8 | flutter.WindowInitialDimensions(800, 1280), 9 | } 10 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/README.md: -------------------------------------------------------------------------------- 1 | # packaging 2 | The template files in the subdirectories are only copied on init and then executed on build. 3 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/darwin-bundle/Info.plist.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | {{.executableName}} 9 | CFBundleGetInfoString 10 | {{.description}} 11 | CFBundleIconFile 12 | icon.icns 13 | NSHighResolutionCapable 14 | 15 | CFBundleIdentifier 16 | {{.organizationName}}.{{.packageName}} 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleLongVersionString 20 | {{.version}} 21 | CFBundleName 22 | {{.applicationName}} 23 | CFBundlePackageType 24 | APPL 25 | CFBundleShortVersionString 26 | {{.version}} 27 | CFBundleSignature 28 | {{.organizationName}}.{{.packageName}} 29 | CFBundleVersion 30 | {{.version}} 31 | CSResourcesFileMapped 32 | 33 | NSHumanReadableCopyright 34 | 35 | NSPrincipalClass 36 | NSApplication 37 | 38 | 39 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/darwin-pkg/Distribution.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{.applicationName}} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | #base.pkg 12 | 13 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/darwin-pkg/PackageInfo.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux-appimage/AppRun.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | exec ./build/{{.executableName}} 4 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux-deb/control.tmpl: -------------------------------------------------------------------------------- 1 | Package: {{.packageName}} 2 | Architecture: amd64 3 | Maintainer: @{{.author}} 4 | Priority: optional 5 | Version: {{.version}} 6 | Description: {{.description}} 7 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux-pkg/PKGBUILD.tmpl: -------------------------------------------------------------------------------- 1 | pkgname={{.packageName}} 2 | pkgver={{.version}} 3 | pkgrel={{.release}} 4 | pkgdesc="{{.description}}" 5 | arch=("x86_64") 6 | license=('{{.license}}') 7 | 8 | package() { 9 | mkdir -p $pkgdir/ 10 | cp * $pkgdir/ -r 11 | } 12 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux-rpm/app.spec.tmpl: -------------------------------------------------------------------------------- 1 | Name: {{.packageName}} 2 | Version: {{.version}} 3 | Release: {{.release}} 4 | Summary: {{.description}} 5 | License: {{.license}} 6 | 7 | %description 8 | {{.description}} 9 | 10 | %install 11 | mkdir -p $RPM_BUILD_ROOT%{_bindir} 12 | mkdir -p $RPM_BUILD_ROOT/usr/lib/{{.packageName}} 13 | mkdir -p $RPM_BUILD_ROOT%{_datadir}/applications 14 | cp -R $RPM_BUILD_DIR/{{.packageName}}-{{.version}}-{{.release}}.x86_64/* $RPM_BUILD_ROOT 15 | chmod 0755 $RPM_BUILD_ROOT%{_bindir}/{{.executableName}} 16 | chmod 0755 $RPM_BUILD_ROOT%{_datadir}/applications/{{.executableName}}.desktop 17 | 18 | %files 19 | %{_bindir}/{{.executableName}} 20 | /usr/lib/{{.packageName}}/ 21 | %{_datadir}/applications/{{.executableName}}.desktop 22 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux-snap/snapcraft.yaml.tmpl: -------------------------------------------------------------------------------- 1 | name: {{.packageName}} 2 | base: core18 3 | version: '{{.version}}' 4 | summary: {{.description}} 5 | description: | 6 | {{.description}} 7 | confinement: devmode 8 | grade: devel 9 | apps: 10 | {{.packageName}}: 11 | command: {{.executableName}} 12 | desktop: local/{{.executableName}}.desktop 13 | parts: 14 | desktop: 15 | plugin: dump 16 | source: snap 17 | assets: 18 | plugin: dump 19 | source: build/assets 20 | app: 21 | plugin: dump 22 | source: build 23 | stage-packages: 24 | - libx11-6 25 | - libxrandr2 26 | - libxcursor1 27 | - libxinerama1 28 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux/app.desktop.tmpl: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Terminal=false 5 | Categories= 6 | Comment={{.description}} 7 | Name={{.applicationName}} 8 | Icon={{.iconPath}} 9 | Exec={{.executablePath}} 10 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/linux/bin.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | /usr/lib/{{.packageName}}/{{.executableName}} 3 | -------------------------------------------------------------------------------- /internal/fileutils/assets/packaging/windows-msi/app.wxs.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /internal/fileutils/assets/plugin/README.md.dlib.tmpl: -------------------------------------------------------------------------------- 1 | The `dlib` folder is used for the plugins which use `cgo`. 2 | 3 | If your go-flutter plugin dose't use `cgo`, just ignore this file and the `dlib` folder. 4 | 5 | When you need to link prebuild dynamic libraries and frameworks, 6 | you should copy the prebuild dynamic libraries and frameworks to `dlib`/${os} folder. 7 | 8 | `hover plugins get` copy this files to path `./go/build/intermediates` of go-flutter app project. 9 | `hover run` copy files from `./go/build/intermediates/${targetOS}` to `./go/build/outputs/${targetOS}`. 10 | And `-L{./go/build/outputs/${targetOS}}` is appended to `cgoLdflags` automatically. 11 | Also `-F{./go/build/outputs/${targetOS}}` is appended to `cgoLdflags` on Mac OS 12 | 13 | Attention: `hover` can't resolve the conflicts 14 | if two different go-flutter plugins have file with the same name in there dlib folder 15 | -------------------------------------------------------------------------------- /internal/fileutils/assets/plugin/README.md.tmpl: -------------------------------------------------------------------------------- 1 | # {{.pluginName}} 2 | 3 | This Go package implements the host-side of the Flutter [{{.pluginName}}](https://{{.urlVSCRepo}}) plugin. 4 | 5 | ## Usage 6 | 7 | Import as: 8 | 9 | ```go 10 | import {{.pluginName}} "{{.urlVSCRepo}}/go" 11 | ``` 12 | 13 | Then add the following option to your go-flutter [application options](https://github.com/go-flutter-desktop/go-flutter/wiki/Plugin-info): 14 | 15 | ```go 16 | flutter.AddPlugin(&{{.pluginName}}.{{.structName}}{}), 17 | ``` 18 | -------------------------------------------------------------------------------- /internal/fileutils/assets/plugin/import.go.tmpl.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // DO NOT EDIT, this file is generated by hover at compile-time for the {{.pluginName}} plugin. 4 | 5 | import ( 6 | flutter "github.com/go-flutter-desktop/go-flutter" 7 | {{.pluginName}} "{{.urlVSCRepo}}/go" 8 | ) 9 | 10 | func init() { 11 | // Only the init function can be tweaked by plugin maker. 12 | options = append(options, flutter.AddPlugin(&{{.pluginName}}.{{.structName}}{})) 13 | } 14 | -------------------------------------------------------------------------------- /internal/fileutils/assets/plugin/plugin.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.pluginName}} 2 | 3 | import ( 4 | flutter "github.com/go-flutter-desktop/go-flutter" 5 | "github.com/go-flutter-desktop/go-flutter/plugin" 6 | ) 7 | 8 | const channelName = "{{.pluginName}}" 9 | 10 | // {{.structName}} implements flutter.Plugin and handles method. 11 | type {{.structName}} struct{} 12 | 13 | var _ flutter.Plugin = &{{.structName}}{} // compile-time type check 14 | 15 | // InitPlugin initializes the plugin. 16 | func (p *{{.structName}}) InitPlugin(messenger plugin.BinaryMessenger) error { 17 | channel := plugin.NewMethodChannel(messenger, channelName, plugin.StandardMethodCodec{}) 18 | channel.HandleFunc("getPlatformVersion", p.handlePlatformVersion) 19 | return nil 20 | } 21 | 22 | func (p *{{.structName}}) handlePlatformVersion(arguments interface{}) (reply interface{}, err error) { 23 | return "go-flutter " + flutter.PlatformVersion, nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/fileutils/embed.go: -------------------------------------------------------------------------------- 1 | package fileutils 2 | 3 | import "embed" 4 | 5 | //go:embed assets 6 | var assets embed.FS 7 | -------------------------------------------------------------------------------- /internal/fileutils/file.go: -------------------------------------------------------------------------------- 1 | package fileutils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "text/template" 13 | 14 | "github.com/go-flutter-desktop/hover/internal/log" 15 | ) 16 | 17 | // IsFileExists checks if a file exists and is not a directory 18 | func IsFileExists(filename string) bool { 19 | info, err := os.Stat(filename) 20 | if os.IsNotExist(err) { 21 | return false 22 | } 23 | return !info.IsDir() 24 | } 25 | 26 | // IsDirectory check if path exists and is a directory 27 | func IsDirectory(path string) bool { 28 | info, err := os.Stat(path) 29 | if os.IsNotExist(err) { 30 | return false 31 | } 32 | return info.IsDir() 33 | } 34 | 35 | // RemoveLinesFromFile removes lines to a file if the text is present in the line 36 | func RemoveLinesFromFile(filePath, text string) { 37 | input, err := ioutil.ReadFile(filePath) 38 | if err != nil { 39 | log.Errorf("Failed to read file %s: %v\n", filePath, err) 40 | os.Exit(1) 41 | } 42 | 43 | lines := strings.Split(string(input), "\n") 44 | 45 | tmp := lines[:0] 46 | for _, line := range lines { 47 | if !strings.Contains(line, text) { 48 | tmp = append(tmp, line) 49 | } 50 | } 51 | output := strings.Join(tmp, "\n") 52 | err = ioutil.WriteFile(filePath, []byte(output), 0644) 53 | if err != nil { 54 | log.Errorf("Failed to write file %s: %v\n", filePath, err) 55 | os.Exit(1) 56 | } 57 | } 58 | 59 | // AddLineToFile appends a newLine to a file if the line isn't 60 | // already present. 61 | func AddLineToFile(filePath, newLine string) { 62 | f, err := os.OpenFile(filePath, 63 | os.O_RDWR|os.O_APPEND, 0660) 64 | if err != nil { 65 | log.Errorf("Failed to open file %s: %v\n", filePath, err) 66 | os.Exit(1) 67 | } 68 | defer f.Close() 69 | content, err := ioutil.ReadAll(f) 70 | if err != nil { 71 | log.Errorf("Failed to read file %s: %v\n", filePath, err) 72 | os.Exit(1) 73 | } 74 | lines := make(map[string]struct{}) 75 | for _, w := range strings.Split(string(content), "\n") { 76 | lines[w] = struct{}{} 77 | } 78 | _, ok := lines[newLine] 79 | if ok { 80 | return 81 | } 82 | if _, err := f.WriteString(newLine + "\n"); err != nil { 83 | log.Errorf("Failed to append '%s' to the file (%s): %v\n", newLine, filePath, err) 84 | os.Exit(1) 85 | } 86 | } 87 | 88 | // CopyFile from one file to another 89 | func CopyFile(src, to string) { 90 | in, err := os.Open(src) 91 | if err != nil { 92 | log.Errorf("Failed to read %s: %v\n", src, err) 93 | os.Exit(1) 94 | } 95 | defer in.Close() 96 | file, err := os.Create(to) 97 | if err != nil { 98 | log.Errorf("Failed to create %s: %v\n", to, err) 99 | os.Exit(1) 100 | } 101 | defer file.Close() 102 | 103 | _, err = io.Copy(file, in) 104 | if err != nil { 105 | log.Errorf("Failed to copy %s to %s: %v\n", src, to, err) 106 | os.Exit(1) 107 | } 108 | } 109 | 110 | // CopyDir copy files from one directory to another directory recursively 111 | func CopyDir(src, dst string) { 112 | var err error 113 | var fds []os.FileInfo 114 | 115 | if !IsDirectory(src) { 116 | log.Errorf("Failed to copy directory, %s not a directory\n", src) 117 | os.Exit(1) 118 | } 119 | 120 | if err = os.MkdirAll(dst, 0755); err != nil { 121 | log.Errorf("Failed to copy directory %s to %s: %v\n", src, dst, err) 122 | os.Exit(1) 123 | } 124 | 125 | if fds, err = ioutil.ReadDir(src); err != nil { 126 | log.Errorf("Failed to list directory %s: %v\n", src, err) 127 | os.Exit(1) 128 | } 129 | 130 | for _, fd := range fds { 131 | srcPath := filepath.Join(src, fd.Name()) 132 | dstPath := filepath.Join(dst, fd.Name()) 133 | if fd.IsDir() { 134 | CopyDir(srcPath, dstPath) 135 | } else { 136 | CopyFile(srcPath, dstPath) 137 | } 138 | } 139 | } 140 | 141 | // CopyTemplateDir copy files from one directory to another directory recursively 142 | // while executing all templates in files and file names 143 | func CopyTemplateDir(boxed, to string, templateData interface{}) { 144 | var files []string 145 | err := filepath.Walk(boxed, func(path string, info os.FileInfo, err error) error { 146 | files = append(files, path) 147 | return nil 148 | }) 149 | files = files[1:] 150 | if err != nil { 151 | log.Errorf("Failed to list files in directory %s: %v\n", boxed, err) 152 | os.Exit(1) 153 | } 154 | for _, file := range files { 155 | newFile := filepath.Join(to, strings.Join(strings.Split(file, "")[len(boxed)+1:], "")) 156 | tmplFile, err := template.New("").Option("missingkey=error").Parse(newFile) 157 | if err != nil { 158 | log.Errorf("Failed to parse template string: %v\n", err) 159 | os.Exit(1) 160 | } 161 | var tmplBytes bytes.Buffer 162 | err = tmplFile.Execute(&tmplBytes, templateData) 163 | if err != nil { 164 | panic(err) 165 | } 166 | newFile = tmplBytes.String() 167 | fi, err := os.Stat(file) 168 | if err != nil { 169 | fmt.Println(err) 170 | return 171 | } 172 | switch mode := fi.Mode(); { 173 | case mode.IsDir(): 174 | err := os.MkdirAll(newFile, 0755) 175 | if err != nil { 176 | log.Errorf("Failed to create directory %s: %v\n", newFile, err) 177 | os.Exit(1) 178 | } 179 | case mode.IsRegular(): 180 | if strings.HasSuffix(newFile, ".tmpl") { 181 | newFile = strings.TrimSuffix(newFile, ".tmpl") 182 | } 183 | ExecuteTemplateFromFile(file, newFile, templateData) 184 | } 185 | } 186 | } 187 | 188 | func executeTemplateFromString(templateString, to string, templateData interface{}) { 189 | tmplFile, err := template.New("").Option("missingkey=error").Parse(templateString) 190 | if err != nil { 191 | log.Errorf("Failed to parse template string: %v\n", err) 192 | os.Exit(1) 193 | } 194 | 195 | toFile, err := os.Create(to) 196 | if err != nil { 197 | log.Errorf("Failed to create '%s': %v\n", to, err) 198 | os.Exit(1) 199 | } 200 | defer toFile.Close() 201 | 202 | tmplFile.Execute(toFile, templateData) 203 | } 204 | 205 | // ExecuteTemplateFromFile create file from a template file 206 | func ExecuteTemplateFromFile(boxed, to string, templateData interface{}) { 207 | templateString, err := ioutil.ReadFile(boxed) 208 | if err != nil { 209 | log.Errorf("Failed to find template file: %v\n", err) 210 | os.Exit(1) 211 | } 212 | executeTemplateFromString(string(templateString), to, templateData) 213 | } 214 | 215 | // ExecuteTemplateFromAssets create file from a template asset 216 | func ExecuteTemplateFromAssets(name, to string, templateData interface{}) { 217 | data, err := assets.ReadFile(fmt.Sprintf("assets/%s", name)) 218 | if err != nil { 219 | log.Errorf("Failed to find template file: %v\n", err) 220 | os.Exit(1) 221 | } 222 | executeTemplateFromString(string(data), to, templateData) 223 | } 224 | 225 | // CopyAsset copies a file from asset 226 | func CopyAsset(name, to string) { 227 | data, err := assets.ReadFile(fmt.Sprintf("assets/%s", name)) 228 | if err != nil { 229 | log.Errorf("Failed to find asset file %s: %v", name, err) 230 | os.Exit(1) 231 | } 232 | err = os.WriteFile(to, data, 0o600) 233 | if err != nil { 234 | log.Errorf("Failed to write file %s: %v", to, err) 235 | os.Exit(1) 236 | } 237 | } 238 | 239 | // DownloadFile will download a url to a local file. 240 | func DownloadFile(url string, filepath string) { 241 | resp, err := http.Get(url) 242 | if err != nil { 243 | log.Errorf("Failed to download '%v': %v\n", url, err) 244 | os.Exit(1) 245 | } 246 | defer resp.Body.Close() 247 | 248 | out, err := os.Create(filepath) 249 | if err != nil { 250 | log.Errorf("Failed to create file '%s': %v\n", filepath, err) 251 | os.Exit(1) 252 | } 253 | defer out.Close() 254 | 255 | _, err = io.Copy(out, resp.Body) 256 | if err != nil { 257 | log.Errorf("Failed to write file '%s': %v\n", filepath, err) 258 | os.Exit(1) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /internal/log/console.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package log 4 | 5 | func EnableColoredConsoleOutput() { 6 | // only windows need special setup 7 | } 8 | -------------------------------------------------------------------------------- /internal/log/console_windows.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "golang.org/x/sys/windows" 5 | "syscall" 6 | ) 7 | 8 | // EnableColoredConsoleOutput sets flag that enables VT escape sequences 9 | // in windows console. See link for Win API documentation. 10 | // 11 | // https://docs.microsoft.com/en-us/windows/console/setconsolemode 12 | // 13 | func EnableColoredConsoleOutput() { 14 | var console_handle syscall.Handle 15 | var err error 16 | 17 | console_handle, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0) 18 | if err != nil { 19 | Errorf("Error getting console handle: %v", err) 20 | return 21 | } 22 | 23 | var mode uint32 24 | err = windows.GetConsoleMode(windows.Handle(console_handle), &mode) 25 | if err != nil { 26 | Errorf("Error getting console mode: %v", err) 27 | return 28 | } 29 | err = windows.SetConsoleMode(windows.Handle(console_handle), mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) 30 | if err != nil { 31 | Errorf("Error setting console mode: %v", err) 32 | return 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/logrusorgru/aurora" 8 | ) 9 | 10 | func init() { 11 | EnableColoredConsoleOutput() 12 | log.SetFlags(0) 13 | } 14 | 15 | // internal colorized 16 | var au aurora.Aurora = aurora.NewAurora(false) 17 | 18 | // internal verbosity level 19 | var verbose bool 20 | 21 | // Verbosity enable verbose logging 22 | func Verbosity(b bool) { 23 | verbose = b 24 | if b { 25 | log.SetFlags(log.Flags() | log.Lshortfile | log.Ltime) 26 | } else { 27 | log.SetFlags(0) 28 | } 29 | } 30 | 31 | // Colorize set the logger to support colors printing. 32 | func Colorize(b bool) { 33 | au = aurora.NewAurora(b) 34 | } 35 | 36 | // Au Aurora instance used for colors 37 | func Au() aurora.Aurora { 38 | return au 39 | } 40 | 41 | // Printf print a message with formatting 42 | func Printf(part string, parts ...interface{}) { 43 | log.Output(2, fmt.Sprint( 44 | hoverPrint(), 45 | fmt.Sprintf(part, parts...), 46 | )) 47 | } 48 | 49 | // Errorf print a error with formatting (red) 50 | func Errorf(part string, parts ...interface{}) { 51 | log.Output(2, fmt.Sprint( 52 | hoverPrint(), 53 | Au().Colorize(fmt.Sprintf(part, parts...), aurora.RedFg).String(), 54 | )) 55 | } 56 | 57 | // Warnf print a warning with formatting (yellow) 58 | func Warnf(part string, parts ...interface{}) { 59 | log.Output(2, fmt.Sprint( 60 | hoverPrint(), 61 | Au().Colorize(fmt.Sprintf(part, parts...), aurora.YellowFg).String(), 62 | )) 63 | } 64 | 65 | // Infof print a information with formatting (green) 66 | func Infof(part string, parts ...interface{}) { 67 | log.Output(2, fmt.Sprint( 68 | hoverPrint(), 69 | Au().Colorize(fmt.Sprintf(part, parts...), aurora.GreenFg).String(), 70 | )) 71 | } 72 | 73 | // Debugf print debugging information with formatting (green) 74 | func Debugf(part string, parts ...interface{}) { 75 | if !verbose { 76 | return 77 | } 78 | 79 | log.Output(2, fmt.Sprint( 80 | hoverPrint(), 81 | Au().Faint(fmt.Sprintf(part, parts...)).String(), 82 | )) 83 | } 84 | 85 | func hoverPrint() string { 86 | return Au().Bold(Au().Cyan("hover: ")).String() 87 | } 88 | -------------------------------------------------------------------------------- /internal/logstreamer/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Kevin van Zonneveld 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /internal/logstreamer/README.md: -------------------------------------------------------------------------------- 1 | logstreamer [![Build Status][BuildStatusIMGURL]][BuildStatusURL] 2 | =============== 3 | [![Flattr][FlattrIMGURL]][FlattrURL] 4 | 5 | [BuildStatusIMGURL]: https://secure.travis-ci.org/kvz/logstreamer.png?branch=master 6 | [BuildStatusURL]: //travis-ci.org/kvz/logstreamer "Build Status" 7 | [FlattrIMGURL]: http://api.flattr.com/button/flattr-badge-large.png 8 | [FlattrURL]: https://flattr.com/submit/auto?user_id=kvz&url=github.com/kvz/logstreamer&title=logstreamer&language=&tags=github&category=software 9 | 10 | Prefixes streams (e.g. stdout or stderr) in Go. 11 | 12 | If you are executing a lot of (remote) commands, you may want to indent all of their 13 | output, prefix the loglines with hostnames, or mark anything that was thrown to stderr 14 | red, so you can spot errors more easily. 15 | 16 | For this purpose, Logstreamer was written. 17 | 18 | You pass 3 arguments to `NewLogstreamer()`: 19 | 20 | - Your `*log.Logger` 21 | - Your desired prefix (`"stdout"` and `"stderr"` prefixed have special meaning) 22 | - If the lines should be recorded `true` or `false`. This is useful if you want to retrieve any errors. 23 | 24 | This returns an interface that you can point `exec.Command`'s `cmd.Stderr` and `cmd.Stdout` to. 25 | All bytes that are written to it are split by newline and then prefixed to your specification. 26 | 27 | **Don't forget to call `Flush()` or `Close()` if the last line of the log 28 | might not end with a newline character!** 29 | 30 | A typical usage pattern looks like this: 31 | 32 | ```go 33 | // Create a logger (your app probably already has one) 34 | logger := log.New(os.Stdout, "--> ", log.Ldate|log.Ltime) 35 | 36 | // Setup a streamer that we'll pipe cmd.Stdout to 37 | logStreamerOut := NewLogstreamer(logger, "stdout", false) 38 | defer logStreamerOut.Close() 39 | // Setup a streamer that we'll pipe cmd.Stderr to. 40 | // We want to record/buffer anything that's written to this (3rd argument true) 41 | logStreamerErr := NewLogstreamer(logger, "stderr", true) 42 | defer logStreamerErr.Close() 43 | 44 | // Execute something that succeeds 45 | cmd := exec.Command( 46 | "ls", 47 | "-al", 48 | ) 49 | cmd.Stderr = logStreamerErr 50 | cmd.Stdout = logStreamerOut 51 | 52 | // Reset any error we recorded 53 | logStreamerErr.FlushRecord() 54 | 55 | // Execute command 56 | err := cmd.Start() 57 | ``` 58 | 59 | ## Test 60 | 61 | ```bash 62 | $ cd src/pkg/logstreamer/ 63 | $ go test 64 | ``` 65 | 66 | Here I issue two local commands, `ls -al` and `ls nonexisting`: 67 | 68 | ![screen shot 2013-07-02 at 2 48 33 pm](https://f.cloud.github.com/assets/26752/736371/16177cf0-e316-11e2-8dc6-320f52f71442.png) 69 | 70 | Over at [Transloadit](http://transloadit.com) we use it for streaming remote commands. 71 | Servers stream command output over SSH back to me, and every line is prefixed with a date, their hostname & marked red in case they 72 | wrote to stderr. 73 | 74 | ## License 75 | 76 | This project is licensed under the MIT license, see `LICENSE.txt`. 77 | -------------------------------------------------------------------------------- /internal/logstreamer/logstreamer.go: -------------------------------------------------------------------------------- 1 | package logstreamer 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type Logstreamer struct { 12 | Logger *log.Logger 13 | buf *bytes.Buffer 14 | // If prefix == stdout, colors green 15 | // If prefix == stderr, colors red 16 | // Else, prefix is taken as-is, and prepended to anything 17 | // you throw at Write() 18 | prefix string 19 | // if true, saves output in memory 20 | record bool 21 | persist string 22 | 23 | // Adds color to stdout & stderr if terminal supports it 24 | colorOkay string 25 | colorFail string 26 | colorReset string 27 | } 28 | 29 | func NewLogstreamerForWriter(prefix string, writer io.Writer) *Logstreamer { 30 | logger := log.New(writer, prefix, 0) 31 | return NewLogstreamer(logger, "", false) 32 | } 33 | 34 | func NewLogstreamerForStdout(prefix string) *Logstreamer { 35 | // logger := log.New(os.Stdout, prefix, log.Ldate|log.Ltime) 36 | logger := log.New(os.Stdout, prefix, 0) 37 | return NewLogstreamer(logger, "", false) 38 | } 39 | 40 | func NewLogstreamerForStderr(prefix string) *Logstreamer { 41 | logger := log.New(os.Stderr, prefix, 0) 42 | return NewLogstreamer(logger, "", false) 43 | } 44 | 45 | func NewLogstreamer(logger *log.Logger, prefix string, record bool) *Logstreamer { 46 | streamer := &Logstreamer{ 47 | Logger: logger, 48 | buf: bytes.NewBuffer([]byte("")), 49 | prefix: prefix, 50 | record: record, 51 | persist: "", 52 | colorOkay: "", 53 | colorFail: "", 54 | colorReset: "", 55 | } 56 | 57 | if strings.HasPrefix(os.Getenv("TERM"), "xterm") { 58 | streamer.colorOkay = "\x1b[32m" 59 | streamer.colorFail = "\x1b[31m" 60 | streamer.colorReset = "\x1b[0m" 61 | } 62 | 63 | return streamer 64 | } 65 | 66 | func (l *Logstreamer) Write(p []byte) (n int, err error) { 67 | if n, err = l.buf.Write(p); err != nil { 68 | return 69 | } 70 | 71 | err = l.OutputLines() 72 | return 73 | } 74 | 75 | func (l *Logstreamer) Close() error { 76 | if err := l.Flush(); err != nil { 77 | return err 78 | } 79 | l.buf = bytes.NewBuffer([]byte("")) 80 | return nil 81 | } 82 | 83 | func (l *Logstreamer) Flush() error { 84 | var p []byte 85 | if _, err := l.buf.Read(p); err != nil { 86 | return err 87 | } 88 | 89 | l.out(string(p)) 90 | return nil 91 | } 92 | 93 | func (l *Logstreamer) OutputLines() error { 94 | for { 95 | line, err := l.buf.ReadString('\n') 96 | 97 | if len(line) > 0 { 98 | if strings.HasSuffix(line, "\n") { 99 | l.out(line) 100 | } else { 101 | // put back into buffer, it's not a complete line yet 102 | // Close() or Flush() have to be used to flush out 103 | // the last remaining line if it does not end with a newline 104 | if _, err := l.buf.WriteString(line); err != nil { 105 | return err 106 | } 107 | } 108 | } 109 | 110 | if err == io.EOF { 111 | break 112 | } 113 | 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (l *Logstreamer) FlushRecord() string { 123 | buffer := l.persist 124 | l.persist = "" 125 | return buffer 126 | } 127 | 128 | func (l *Logstreamer) out(str string) { 129 | if len(str) < 1 { 130 | return 131 | } 132 | 133 | if l.record == true { 134 | l.persist = l.persist + str 135 | } 136 | 137 | if l.prefix == "stdout" { 138 | str = l.colorOkay + l.prefix + l.colorReset + " " + str 139 | } else if l.prefix == "stderr" { 140 | str = l.colorFail + l.prefix + l.colorReset + " " + str 141 | } else { 142 | str = l.prefix + str 143 | } 144 | 145 | l.Logger.Print(str) 146 | } 147 | -------------------------------------------------------------------------------- /internal/logstreamer/logstreamer_test.go: -------------------------------------------------------------------------------- 1 | package logstreamer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "testing" 9 | ) 10 | 11 | func TestLogstreamerOk(t *testing.T) { 12 | // Create a logger (your app probably already has one) 13 | logger := log.New(os.Stdout, "--> ", log.Ldate|log.Ltime) 14 | 15 | // Setup a streamer that we'll pipe cmd.Stdout to 16 | logStreamerOut := NewLogstreamer(logger, "stdout", false) 17 | defer logStreamerOut.Close() 18 | // Setup a streamer that we'll pipe cmd.Stderr to. 19 | // We want to record/buffer anything that's written to this (3rd argument true) 20 | logStreamerErr := NewLogstreamer(logger, "stderr", true) 21 | defer logStreamerErr.Close() 22 | 23 | // Execute something that succeeds 24 | cmd := exec.Command( 25 | "ls", 26 | "-al", 27 | ) 28 | cmd.Stderr = logStreamerErr 29 | cmd.Stdout = logStreamerOut 30 | 31 | // Reset any error we recorded 32 | logStreamerErr.FlushRecord() 33 | 34 | // Execute command 35 | err := cmd.Start() 36 | 37 | // Failed to spawn? 38 | if err != nil { 39 | t.Fatal("ERROR could not spawn command.", err.Error()) 40 | } 41 | 42 | // Failed to execute? 43 | err = cmd.Wait() 44 | if err != nil { 45 | t.Fatal("ERROR command finished with error. ", err.Error(), logStreamerErr.FlushRecord()) 46 | } 47 | } 48 | 49 | func TestLogstreamerErr(t *testing.T) { 50 | // Create a logger (your app probably already has one) 51 | logger := log.New(os.Stdout, "--> ", log.Ldate|log.Ltime) 52 | 53 | // Setup a streamer that we'll pipe cmd.Stdout to 54 | logStreamerOut := NewLogstreamer(logger, "stdout", false) 55 | defer logStreamerOut.Close() 56 | // Setup a streamer that we'll pipe cmd.Stderr to. 57 | // We want to record/buffer anything that's written to this (3rd argument true) 58 | logStreamerErr := NewLogstreamer(logger, "stderr", true) 59 | defer logStreamerErr.Close() 60 | 61 | // Execute something that succeeds 62 | cmd := exec.Command( 63 | "ls", 64 | "nonexisting", 65 | ) 66 | cmd.Stderr = logStreamerErr 67 | cmd.Stdout = logStreamerOut 68 | 69 | // Reset any error we recorded 70 | logStreamerErr.FlushRecord() 71 | 72 | // Execute command 73 | err := cmd.Start() 74 | 75 | // Failed to spawn? 76 | if err != nil { 77 | logger.Print("ERROR could not spawn command. ") 78 | } 79 | 80 | // Failed to execute? 81 | err = cmd.Wait() 82 | if err != nil { 83 | fmt.Printf("Good. command finished with %s. %s. \n", err.Error(), logStreamerErr.FlushRecord()) 84 | } else { 85 | t.Fatal("This command should have failed") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/modx/.fixtures/example1/empty.go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /internal/modx/.fixtures/example1/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | github.com/spf13/cobra v1.9.1 8 | ) 9 | 10 | 11 | replace github.com/pkg/errors => /tmp/errors -------------------------------------------------------------------------------- /internal/modx/.fixtures/example1/output1.go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.13 4 | 5 | require github.com/spf13/cobra v1.0.0 6 | -------------------------------------------------------------------------------- /internal/modx/modx.go: -------------------------------------------------------------------------------- 1 | package modx 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/pkg/errors" 9 | "golang.org/x/mod/modfile" 10 | "golang.org/x/mod/module" 11 | ) 12 | 13 | // Open the go.mod file in the given directory 14 | func Open(dir string) (m *modfile.File, err error) { 15 | if dir, err = FindModuleRoot(dir); err != nil { 16 | return m, err 17 | } 18 | 19 | goModPath := filepath.Join(dir, "go.mod") 20 | 21 | goModBytes, err := ioutil.ReadFile(goModPath) 22 | if err != nil && !os.IsNotExist(err) { 23 | return m, errors.Wrapf(err, "failed to read the 'go.mod' file: %v", goModPath) 24 | } 25 | 26 | if m, err = modfile.Parse(goModPath, goModBytes, nil); err != nil { 27 | return m, errors.Wrapf(err, "failed to read the 'go.mod' file: %v", goModPath) 28 | } 29 | 30 | return m, nil 31 | } 32 | 33 | // Version locates the module version for the given import path. 34 | // returns zero version if none are found. 35 | // Version differs from find in that it returns the version in use 36 | // vs the version required. 37 | func Version(m *modfile.File, path string) module.Version { 38 | for _, pkg := range m.Replace { 39 | if pkg.Old.Path == path { 40 | return pkg.New 41 | } 42 | } 43 | 44 | for _, pkg := range m.Require { 45 | if pkg.Mod.Path == path { 46 | return pkg.Mod 47 | } 48 | } 49 | 50 | return module.Version{} 51 | } 52 | 53 | // Find locates the versions for the given import path. 54 | // returns zero version if none are found. 55 | func Find(m *modfile.File, path string) module.Version { 56 | for _, pkg := range m.Replace { 57 | if pkg.Old.Path == path { 58 | return pkg.Old 59 | } 60 | } 61 | 62 | for _, pkg := range m.Require { 63 | if pkg.Mod.Path == path { 64 | return pkg.Mod 65 | } 66 | } 67 | 68 | return module.Version{} 69 | } 70 | 71 | // RemoveModule drop a module from go.mod entirely. 72 | func RemoveModule(m *modfile.File, path string) error { 73 | for v := Find(m, path); v.Path != ""; v = Find(m, path) { 74 | err := m.DropReplace(v.Path, v.Version) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | err = m.DropRequire(v.Path) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // Mutate the go.mod file in the given directory. 89 | func Mutate(dir string, mutation func(*modfile.File) error) (err error) { 90 | var mod *modfile.File 91 | 92 | if mod, err = Open(dir); err != nil { 93 | return err 94 | } 95 | 96 | if err = mutation(mod); err != nil { 97 | return err 98 | } 99 | 100 | return Replace(dir, mod) 101 | } 102 | 103 | // Replace the go.mod file in the given directory. 104 | func Replace(dir string, m *modfile.File) (err error) { 105 | m.Cleanup() 106 | 107 | if dir, err = FindModuleRoot(dir); err != nil { 108 | return err 109 | } 110 | 111 | goModPath := filepath.Join(dir, "go.mod") 112 | 113 | out, err := m.Format() 114 | if err != nil { 115 | return errors.Wrapf(err, "failed to format the 'go.mod' file: %s", goModPath) 116 | } 117 | 118 | info, err := os.Stat(goModPath) 119 | if err != nil { 120 | return errors.Wrapf(err, "failed to stat the 'go.mod' file: %s", goModPath) 121 | } 122 | 123 | err = ioutil.WriteFile(goModPath, out, info.Mode().Perm()) 124 | if err != nil { 125 | return errors.Wrapf(err, "failed to update the 'go.mod' file: %s", goModPath) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | // Print the modfile. 132 | func Print(m *modfile.File) (s string, err error) { 133 | m.Cleanup() 134 | out, err := m.Format() 135 | if err != nil { 136 | return "", errors.Wrapf(err, "failed to format the 'go.mod' file") 137 | } 138 | 139 | return string(out), nil 140 | } 141 | 142 | // FindModuleRoot pulled from: https://github.com/golang/go/blob/88e564edb13f1596c12ad16d5fd3c7ac7deac855/src/cmd/dist/build.go#L1595 143 | func FindModuleRoot(dir string) (cleaned string, err error) { 144 | if dir == "" { 145 | return "", errors.New("cannot located go.mod from a blank directory path") 146 | } 147 | 148 | if cleaned, err = filepath.Abs(filepath.Clean(dir)); err != nil { 149 | return "", errors.Wrap(err, "failed to determined absolute path to directory") 150 | } 151 | 152 | // Look for enclosing go.mod. 153 | for { 154 | gomod := filepath.Join(cleaned, "go.mod") 155 | if fi, err := os.Stat(gomod); err == nil && !fi.IsDir() { 156 | return cleaned, nil 157 | } 158 | 159 | d := filepath.Dir(cleaned) 160 | 161 | if d == cleaned { 162 | break 163 | } 164 | 165 | cleaned = d 166 | } 167 | 168 | return "", errors.Errorf("go.mod not found: %s", dir) 169 | } 170 | -------------------------------------------------------------------------------- /internal/modx/modx_test.go: -------------------------------------------------------------------------------- 1 | package modx 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRemoveModule(t *testing.T) { 11 | gomod, err := Open(".fixtures/example1") 12 | require.Equal(t, err, nil, "unable to open go.mod: %v", err) 13 | 14 | // should succeed when the module doesn't exist. 15 | err = RemoveModule(gomod, "") 16 | require.Equal(t, err, nil, "failed to remove blank module path: %v", err) 17 | 18 | // remove all defined modules 19 | for _, v := range gomod.Require { 20 | err = RemoveModule(gomod, v.Mod.Path) 21 | require.Equal(t, err, nil, "failed to remove module: %s - %v", v.Mod.Path, err) 22 | } 23 | 24 | output, err := Print(gomod) 25 | require.Equal(t, err, nil, "failed to print go.mod %v", err) 26 | 27 | expected, err := ioutil.ReadFile(".fixtures/example1/empty.go.mod") 28 | require.Equal(t, err, nil, "failed to read fixture %v", err) 29 | require.Equal(t, output, string(expected)) 30 | } 31 | 32 | func TestRemoveModuleIdempotent(t *testing.T) { 33 | const module = "github.com/pkg/errors" 34 | gomod, err := Open(".fixtures/example1") 35 | require.Equal(t, err, nil, "unable to open go.mod: %v", err) 36 | 37 | err = RemoveModule(gomod, module) 38 | require.Equal(t, err, nil, "failed to remove import %s", module) 39 | 40 | err = RemoveModule(gomod, module) 41 | require.Equal(t, err, nil, "failed to remove import %s", module) 42 | 43 | output, err := Print(gomod) 44 | require.Equal(t, err, nil, "failed to print go.mod %v", err) 45 | 46 | expected, err := ioutil.ReadFile(".fixtures/example1/output1.go.mod") 47 | require.Equal(t, err, nil, "failed to read fixture %v", err) 48 | require.Equal(t, output, string(expected)) 49 | } 50 | -------------------------------------------------------------------------------- /internal/pubspec/pubspec.go: -------------------------------------------------------------------------------- 1 | package pubspec 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-flutter-desktop/hover/internal/config" 6 | "os" 7 | "os/user" 8 | 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/go-flutter-desktop/hover/internal/log" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // PubSpec contains the parsed contents of pubspec.yaml 16 | type PubSpec struct { 17 | Name string 18 | Description string 19 | Version string 20 | Author string 21 | Dependencies map[string]interface{} 22 | Flutter map[string]interface{} 23 | } 24 | 25 | func (p PubSpec) GetDescription() string { 26 | if len(p.Description) == 0 { 27 | p.Description = "A flutter app made with go-flutter" 28 | config.PrintMissingField("description", "pubspec.yaml", p.Description) 29 | } 30 | return p.Description 31 | } 32 | 33 | func (p PubSpec) GetVersion() string { 34 | if len(p.Version) == 0 { 35 | p.Version = "0.0.1" 36 | config.PrintMissingField("version", "pubspec.yaml", p.Version) 37 | } 38 | return p.Version 39 | } 40 | 41 | func (p PubSpec) GetAuthor() string { 42 | if len(p.Author) == 0 { 43 | u, err := user.Current() 44 | if err != nil { 45 | log.Errorf("Couldn't get current user: %v", err) 46 | os.Exit(1) 47 | } 48 | p.Author = u.Username 49 | config.PrintMissingField("author", "pubspec.yaml", p.Author) 50 | } 51 | return p.Author 52 | } 53 | 54 | var pubspec = PubSpec{} 55 | 56 | // GetPubSpec returns the working directory pubspec.yaml as a PubSpec 57 | func GetPubSpec() PubSpec { 58 | if pubspec.Name == "" { 59 | pub, err := ReadPubSpecFile("pubspec.yaml") 60 | if err != nil { 61 | log.Errorf("%v", err) 62 | log.Errorf("This command should be run from the root of your Flutter project.") 63 | os.Exit(1) 64 | } 65 | pubspec = *pub 66 | } 67 | return pubspec 68 | } 69 | 70 | // ReadPubSpecFile reads a .yaml file at a path and return a correspond 71 | // PubSpec struct 72 | func ReadPubSpecFile(pubSpecPath string) (*PubSpec, error) { 73 | file, err := os.Open(pubSpecPath) 74 | if err != nil { 75 | if os.IsNotExist(err) { 76 | return nil, errors.Wrap(err, "Error: No pubspec.yaml file found") 77 | } 78 | return nil, errors.Wrap(err, "Failed to open pubspec.yaml") 79 | } 80 | defer file.Close() 81 | 82 | var pub PubSpec 83 | err = yaml.NewDecoder(file).Decode(&pub) 84 | if err != nil { 85 | return nil, errors.Wrap(err, "Failed to decode pubspec.yaml") 86 | } 87 | // avoid checking for the flutter dependencies for out of ws directories 88 | if pubSpecPath != "pubspec.yaml" { 89 | return &pub, nil 90 | } 91 | if _, exists := pub.Dependencies["flutter"]; !exists { 92 | return nil, errors.New(fmt.Sprintf("Missing `flutter` in %s dependencies list", pubSpecPath)) 93 | } 94 | return &pub, nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "os" 7 | "os/exec" 8 | "runtime/debug" 9 | "sync" 10 | 11 | "github.com/go-flutter-desktop/hover/internal/build" 12 | "github.com/go-flutter-desktop/hover/internal/log" 13 | ) 14 | 15 | // FlutterRequiredEngineVersion returns the commit hash of the engine in use 16 | func FlutterRequiredEngineVersion() string { 17 | return readFlutterVersion().EngineRevision 18 | } 19 | 20 | // FlutterChannel returns the channel of the flutter installation 21 | func FlutterChannel() string { 22 | return readFlutterVersion().Channel 23 | } 24 | 25 | func readFlutterVersion() flutterVersionResponse { 26 | out, err := exec.Command(build.FlutterBin(), "--version", "--machine").Output() 27 | if err != nil { 28 | log.Errorf("Failed to run %s: %v", log.Au().Magenta("flutter --version --machine"), err) 29 | os.Exit(1) 30 | } 31 | 32 | // Read bytes from the stdout until we receive what looks like the start of 33 | // a valid json object. This code may be removed when the following flutter 34 | // issue is resolved. https://github.com/flutter/flutter/issues/54014 35 | outputBuffer := bytes.NewBuffer(out) 36 | for { 37 | b, err := outputBuffer.ReadByte() 38 | if err != nil { 39 | log.Errorf("Failed to run %s: did not return information in json", log.Au().Magenta("flutter --version --machine")) 40 | os.Exit(1) 41 | } 42 | if b == '{' { 43 | outputBuffer.UnreadByte() 44 | break 45 | } 46 | } 47 | 48 | var response flutterVersionResponse 49 | err = json.NewDecoder(outputBuffer).Decode(&response) 50 | if err != nil { 51 | log.Errorf("Failed parsing json: %v", err) 52 | os.Exit(1) 53 | } 54 | return response 55 | } 56 | 57 | type flutterVersionResponse struct { 58 | Channel string 59 | EngineRevision string 60 | } 61 | 62 | var ( 63 | hoverVersionValue string 64 | hoverVersionOnce sync.Once 65 | ) 66 | 67 | func HoverVersion() string { 68 | hoverVersionOnce.Do(func() { 69 | buildInfo, ok := debug.ReadBuildInfo() 70 | if !ok { 71 | log.Errorf("Cannot obtain version information from hover build. To resolve this, please go-get hover using Go 1.13 or newer.") 72 | os.Exit(1) 73 | } 74 | hoverVersionValue = buildInfo.Main.Version 75 | }) 76 | return hoverVersionValue 77 | } 78 | -------------------------------------------------------------------------------- /internal/versioncheck/version.go: -------------------------------------------------------------------------------- 1 | package versioncheck 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/tcnksm/go-latest" 14 | "golang.org/x/mod/modfile" 15 | "golang.org/x/mod/module" 16 | 17 | "github.com/go-flutter-desktop/hover/internal/fileutils" 18 | "github.com/go-flutter-desktop/hover/internal/log" 19 | "github.com/go-flutter-desktop/hover/internal/modx" 20 | ) 21 | 22 | func hasUpdate(timestampDir, currentVersion, repo string) (bool, string) { 23 | cachedCheckPath := filepath.Join(timestampDir, fmt.Sprintf(".last_%s_check", repo)) 24 | cachedCheckBytes, err := ioutil.ReadFile(cachedCheckPath) 25 | if err != nil && !os.IsNotExist(err) { 26 | log.Warnf("Failed to read the %s last update check: %v", repo, err) 27 | return false, "" 28 | } 29 | 30 | cachedCheck := string(cachedCheckBytes) 31 | cachedCheck = strings.TrimSuffix(cachedCheck, "\n") 32 | 33 | now := time.Now() 34 | nowString := strconv.FormatInt(now.Unix(), 10) 35 | 36 | if cachedCheck == "" { 37 | err = ioutil.WriteFile(cachedCheckPath, []byte(nowString), 0664) 38 | if err != nil { 39 | log.Warnf("Failed to write the update timestamp: %v", err) 40 | } 41 | 42 | return false, "" 43 | } 44 | 45 | i, err := strconv.ParseInt(cachedCheck, 10, 64) 46 | if err != nil { 47 | log.Warnf("Failed to parse the last update of %s: %v", repo, err) 48 | return false, "" 49 | } 50 | lastUpdateTimeStamp := time.Unix(i, 0) 51 | 52 | checkRate := 1.0 53 | 54 | newCheck := now.Sub(lastUpdateTimeStamp).Hours() > checkRate || 55 | (now.Sub(lastUpdateTimeStamp).Minutes() < 1.0 && // keep the notice for X Minutes 56 | now.Sub(lastUpdateTimeStamp).Minutes() > 0.0) 57 | 58 | checkUpdateOptOut := os.Getenv("HOVER_IGNORE_CHECK_NEW_RELEASE") 59 | if newCheck && checkUpdateOptOut != "true" { 60 | log.Printf("Checking available release on Github") 61 | 62 | // fetch the last githubTag 63 | githubTag := &latest.GithubTag{ 64 | Owner: "go-flutter-desktop", 65 | Repository: repo, 66 | FixVersionStrFunc: latest.DeleteFrontV(), 67 | } 68 | 69 | res, err := latest.Check(githubTag, currentVersion) 70 | if err != nil { 71 | log.Warnf("Failed to check the latest release of '%s': %v", repo, err) 72 | 73 | // update the timestamp 74 | // don't spam people who don't have access to internet 75 | now := time.Now().Add(time.Duration(checkRate) * time.Hour) 76 | nowString := strconv.FormatInt(now.Unix(), 10) 77 | 78 | err = ioutil.WriteFile(cachedCheckPath, []byte(nowString), 0664) 79 | if err != nil { 80 | log.Warnf("Failed to write the update timestamp to file: %v", err) 81 | } 82 | 83 | return false, "" 84 | } 85 | 86 | if now.Sub(lastUpdateTimeStamp).Hours() > checkRate { 87 | // update the timestamp 88 | err = ioutil.WriteFile(cachedCheckPath, []byte(nowString), 0664) 89 | if err != nil { 90 | log.Warnf("Failed to write the update timestamp to file: %v", err) 91 | } 92 | } 93 | return res.Outdated, res.Current 94 | } 95 | return false, "" 96 | } 97 | 98 | // CheckForHoverUpdate check the last 'hover' timestamp we have cached. 99 | // If the last update comes back to more than X days, 100 | // fetch the last Github release semver. If the Github semver is more recent 101 | // than the current one, display the update notice. 102 | func CheckForHoverUpdate(currentVersion string) { 103 | // Don't check for updates if installed from local 104 | if currentVersion != "(devel)" { 105 | cacheDir, err := os.UserCacheDir() 106 | if err != nil { 107 | log.Errorf("Failed to get cache directory: %v", err) 108 | os.Exit(1) 109 | } 110 | update, newVersion := hasUpdate(filepath.Join(cacheDir, "hover"), currentVersion, "hover") 111 | if update { 112 | log.Infof("'hover' has an update available. (%s -> %s)", currentVersion, newVersion) 113 | log.Infof(" To update 'hover' go to `https://github.com/go-flutter-desktop/hover#install` and follow the install steps") 114 | } 115 | } 116 | } 117 | 118 | // CheckForGoFlutterUpdate check the last 'go-flutter' timestamp we have cached 119 | // for the current project. If the last update comes back to more than X days, 120 | // fetch the last Github release semver. If the Github semver is more recent 121 | // than the current one, display the update notice. 122 | func CheckForGoFlutterUpdate(goDirectoryPath string, currentTag string) { 123 | hoverGitignore := filepath.Join(goDirectoryPath, ".gitignore") 124 | fileutils.AddLineToFile(hoverGitignore, ".last_go-flutter_check") 125 | update, newVersion := hasUpdate(goDirectoryPath, currentTag, "go-flutter") 126 | if update { 127 | log.Infof("The core library 'go-flutter' has an update available. (%s -> %s)", currentTag, newVersion) 128 | log.Infof(" To update 'go-flutter' in this project run: `%s`", log.Au().Magenta("hover bumpversion")) 129 | } 130 | } 131 | 132 | // CurrentGoFlutterTag retrieve the semver of go-flutter in 'go.mod' 133 | func CurrentGoFlutterTag(goDirectoryPath string) (currentTag string, err error) { 134 | const expected = "github.com/go-flutter-desktop/go-flutter" 135 | var m *modfile.File 136 | 137 | if m, err = modx.Open(goDirectoryPath); err != nil { 138 | return "", err 139 | } 140 | 141 | if v := modx.Version(m, expected); (v != module.Version{}) { 142 | return v.Version, nil 143 | } 144 | 145 | return "", errors.New("failed to parse the 'go-flutter' version in go.mod") 146 | } 147 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-flutter-desktop/hover/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | }, 7 | "gomodTidy": true, 8 | "requiredStatusChecks": null, 9 | "postUpdateOptions": [ 10 | "gomodTidy" 11 | ], 12 | "ignoreDeps": [ 13 | "golang.org/x/crypto", 14 | "golang.org/x/net", 15 | "golang.org/x/sys", 16 | "github.com/google/go-github" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------