├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------