diff --git a/README.md b/README.md index b6970056..dd3d8295 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,15 @@ If you want to integrate go-flutter with VSCode, read this [issue](https://githu ### Build standalone application -To create a standalone debug build run this command: +To create a standalone release (JIT mode) build run this command: ```bash hover build linux # or darwin or windows ``` +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/). +Then just run the command from above and it will do everything for you. -The output will be in `go/build/outputs/linux` or windows or darwin depending on your OS. Hover does not yet support cross-compilation. +The output will be in `go/build/outputs/linux` or windows or darwin. To start the binary: (replace `yourApplicationName` with your app name) diff --git a/cmd/build.go b/cmd/build.go index 94c93575..3c103b45 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "os/user" "path/filepath" "regexp" "runtime" @@ -27,16 +28,24 @@ var ( buildCachePath string buildOmitEmbedder bool buildOmitFlutterBundle bool + buildDocker bool ) const buildPath = "go" +const mingwGccBinName = "x86_64-w64-mingw32-gcc" +const clangBinName = "o32-clang" + +var crossCompile = false +var engineCachePath string + func init() { buildCmd.PersistentFlags().StringVarP(&buildTarget, "target", "t", "lib/main_desktop.dart", "The main entry-point file of the application.") buildCmd.PersistentFlags().StringVarP(&buildManifest, "manifest", "m", "pubspec.yaml", "Flutter manifest file of the application.") buildCmd.PersistentFlags().StringVarP(&buildBranch, "branch", "b", "", "The 'go-flutter' version to use. (@master or @v0.20.0 for example)") buildCmd.PersistentFlags().BoolVar(&buildDebug, "debug", false, "Build a debug version of the app.") buildCmd.PersistentFlags().StringVarP(&buildCachePath, "cache-path", "", "", "The path that hover uses to cache dependencies such as the Flutter engine .so/.dll (defaults to the standard user cache directory)") + buildCmd.PersistentFlags().BoolVar(&buildDocker, "docker", false, "Compile in Docker container only. No need to install go") buildCmd.AddCommand(buildLinuxCmd) buildCmd.AddCommand(buildLinuxSnapCmd) buildCmd.AddCommand(buildLinuxDebCmd) @@ -146,12 +155,107 @@ func outputBinaryPath(projectName string, targetOS string) string { return outputBinaryPath } -func build(projectName string, targetOS string, vmArguments []string) { - if targetOS != runtime.GOOS { - fmt.Println("hover: Cross-compiling is currently not supported") +func dockerBuild(projectName string, targetOS string, vmArguments []string) { + crossCompilingDir, err := filepath.Abs(filepath.Join(buildPath, "cross-compiling")) + err = os.MkdirAll(crossCompilingDir, 0755) + if err != nil { + fmt.Printf("hover: Cannot create the cross-compiling directory: %v\n", err) + os.Exit(1) + } + userCacheDir, err := os.UserCacheDir() + if err != nil { + fmt.Printf("hover: Cannot get the path for the system cache directory: %v\n", err) + os.Exit(1) + } + goPath := filepath.Join(userCacheDir, "hover-cc") + err = os.MkdirAll(goPath, 0755) + if err != nil { + fmt.Printf("hover: Cannot create the hover-cc GOPATH under the system cache directory: %v\n", err) + os.Exit(1) + } + wd, err := os.Getwd() + if err != nil { + fmt.Printf("hover: Cannot get the path for current directory %s", err) os.Exit(1) } - var engineCachePath string + dockerFilePath, err := filepath.Abs(filepath.Join(crossCompilingDir, "Dockerfile")) + if err != nil { + fmt.Printf("hover: Failed to resolve absolute path for Dockerfile %s: %v\n", dockerFilePath, err) + os.Exit(1) + } + if _, err := os.Stat(dockerFilePath); os.IsNotExist(err) { + dockerFile, err := os.Create(dockerFilePath) + if err != nil { + fmt.Printf("hover: Failed to create Dockerfile %s: %v\n", dockerFilePath, err) + os.Exit(1) + } + dockerFileContent := []string{ + "FROM dockercore/golang-cross", + "RUN apt-get install libgl1-mesa-dev xorg-dev -y", + } + + for _, line := range dockerFileContent { + if _, err := dockerFile.WriteString(line + "\n"); err != nil { + fmt.Printf("hover: Could not write Dockerfile: %v\n", err) + os.Exit(1) + } + } + err = dockerFile.Close() + if err != nil { + fmt.Printf("hover: Could not close Dockerfile: %v\n", err) + os.Exit(1) + } + fmt.Printf("hover: A Dockerfile for cross-compiling for %s has been created at %s. You can add it to git.\n", targetOS, filepath.Join(buildPath, "cross-compiling", targetOS)) + } + dockerBuildCmd := exec.Command(dockerBin, "build", "-t", "hover-build-cc", ".") + dockerBuildCmd.Stderr = os.Stderr + dockerBuildCmd.Dir = crossCompilingDir + err = dockerBuildCmd.Run() + if err != nil { + fmt.Printf("hover: Docker build failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("hover: Cross-Compiling 'go-flutter' and plugins using docker") + + u, err := user.Current() + if err != nil { + fmt.Printf("hover: Couldn't get current user: %v\n", err) + os.Exit(1) + } + args := []string{ + "run", + "-w", "/app/go", + "-v", goPath + ":/go", + "-v", wd + ":/app", + "-v", engineCachePath + ":/engine", + "-v", filepath.Join(userCacheDir, "go-build") + ":/cache", + } + for _, env := range buildEnv(targetOS, "/engine") { + args = append(args, "-e", env) + } + args = append(args, "hover-build-cc") + chownStr := "" + if runtime.GOOS != "windows" { + chownStr = fmt.Sprintf(" && chown %s:%s build/ -R", u.Uid, u.Gid) + } + args = append(args, "bash", "-c", fmt.Sprintf("%s%s", strings.Join(buildCommand(targetOS, vmArguments, "build/outputs/"+targetOS+"/"+outputBinaryName(projectName, targetOS)), " "), chownStr)) + dockerRunCmd := exec.Command(dockerBin, args...) + dockerRunCmd.Stderr = os.Stderr + dockerRunCmd.Stdout = os.Stdout + dockerRunCmd.Dir = crossCompilingDir + err = dockerRunCmd.Run() + if err != nil { + fmt.Printf("hover: Docker run failed: %v\n", err) + os.Exit(1) + } + fmt.Println("hover: Successfully cross-compiled for " + targetOS) +} + +func build(projectName string, targetOS string, vmArguments []string) { + crossCompile = targetOS != runtime.GOOS + buildDocker = crossCompile || buildDocker + if buildCachePath != "" { engineCachePath = enginecache.ValidateOrUpdateEngineAtPath(targetOS, buildCachePath) } else { @@ -265,19 +369,6 @@ func build(projectName string, targetOS string, vmArguments []string) { return } - var cgoLdflags string - switch targetOS { - case "darwin": - cgoLdflags = fmt.Sprintf("-F%s -Wl,-rpath,@executable_path", engineCachePath) - case "linux": - cgoLdflags = fmt.Sprintf("-L%s", engineCachePath) - case "windows": - cgoLdflags = fmt.Sprintf("-L%s", engineCachePath) - default: - fmt.Printf("hover: Target platform %s is not supported, cgo_ldflags not implemented.\n", targetOS) - os.Exit(1) - } - wd, err := os.Getwd() if err != nil { fmt.Printf("hover: Failed to get working dir: %v\n", err) @@ -323,28 +414,19 @@ func build(projectName string, targetOS string, vmArguments []string) { } } - var ldflags []string - if !buildDebug { - vmArguments = append(vmArguments, "--disable-dart-asserts") - vmArguments = append(vmArguments, "--disable-observatory") - - if targetOS == "windows" { - ldflags = append(ldflags, "-H=windowsgui") + if buildDocker { + if crossCompile { + fmt.Printf("hover: Because %s is not able to compile for %s out of the box, a cross-compiling container is used\n", runtime.GOOS, targetOS) } - ldflags = append(ldflags, "-s") - ldflags = append(ldflags, "-w") + dockerBuild(projectName, targetOS, vmArguments) + return } - ldflags = append(ldflags, fmt.Sprintf("-X main.vmArguments=%s", strings.Join(vmArguments, ";"))) - cmdGoBuild := exec.Command(goBin, "build", - "-o", outputBinaryPath(projectName, targetOS), - fmt.Sprintf("-ldflags=%s", strings.Join(ldflags, " ")), - dotSlash+"cmd", - ) + buildCommandString := buildCommand(targetOS, vmArguments, outputBinaryPath(projectName, targetOS)) + cmdGoBuild := exec.Command(buildCommandString[0], buildCommandString[1:]...) cmdGoBuild.Dir = filepath.Join(wd, buildPath) cmdGoBuild.Env = append(os.Environ(), - "GO111MODULE=on", - "CGO_LDFLAGS="+cgoLdflags, + buildEnv(targetOS, engineCachePath)..., ) cmdGoBuild.Stderr = os.Stderr @@ -356,4 +438,71 @@ func build(projectName string, targetOS string, vmArguments []string) { fmt.Printf("hover: Go build failed: %v\n", err) os.Exit(1) } + fmt.Println("hover: Successfully compiled") +} + +func buildEnv(targetOS string, engineCachePath string) []string { + var cgoLdflags string + switch targetOS { + case "darwin": + cgoLdflags = fmt.Sprintf("-F%s -Wl,-rpath,@executable_path", engineCachePath) + case "linux": + cgoLdflags = fmt.Sprintf("-L%s", engineCachePath) + case "windows": + cgoLdflags = fmt.Sprintf("-L%s", engineCachePath) + default: + fmt.Printf("hover: Target platform %s is not supported, cgo_ldflags not implemented.\n", targetOS) + os.Exit(1) + } + env := []string{ + "GO111MODULE=on", + "CGO_LDFLAGS=" + cgoLdflags, + "GOOS=" + targetOS, + "GOARCH=amd64", + "CGO_ENABLED=1", + } + if buildDocker { + env = append(env, + "GOCACHE=/cache", + ) + if targetOS == "windows" { + env = append(env, + "CC="+mingwGccBinName, + ) + } + if targetOS == "darwin" { + env = append(env, + "CC="+clangBinName, + ) + } + } + return env +} + +func buildCommand(targetOS string, vmArguments []string, outputBinaryPath string) []string { + var ldflags []string + if !buildDebug { + vmArguments = append(vmArguments, "--disable-dart-asserts") + vmArguments = append(vmArguments, "--disable-observatory") + + if targetOS == "windows" { + ldflags = append(ldflags, "-H=windowsgui") + } + ldflags = append(ldflags, "-s") + ldflags = append(ldflags, "-w") + } + ldflags = append(ldflags, fmt.Sprintf("-X main.vmArguments=%s", strings.Join(vmArguments, ";"))) + outputCommand := []string{ + "go", + "build", + "-o", outputBinaryPath, + "-v", + } + if buildDocker { + outputCommand = append(outputCommand, fmt.Sprintf("-ldflags=\"%s\"", strings.Join(ldflags, " "))) + } else { + outputCommand = append(outputCommand, fmt.Sprintf("-ldflags=%s", strings.Join(ldflags, " "))) + } + outputCommand = append(outputCommand, dotSlash+"cmd") + return outputCommand } diff --git a/cmd/common.go b/cmd/common.go index 1675ff89..59220639 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -19,13 +19,27 @@ func init() { var ( goBin string flutterBin string + dockerBin string ) func initBinaries() { var err error + goAvailable := false + dockerAvailable := false goBin, err = exec.LookPath("go") - if err != nil { - fmt.Println("hover: Failed to lookup `go` executable. Please install Go.\nhttps://golang.org/doc/install") + if err == nil { + goAvailable = true + } + dockerBin, err = exec.LookPath("docker") + if err == nil { + dockerAvailable = true + } + if !dockerAvailable && !goAvailable { + fmt.Println("hover: Failed to lookup `go` and `docker` executable. Please install one of them:\nGo: https://golang.org/doc/install\nDocker: https://docs.docker.com/install") + os.Exit(1) + } + if dockerAvailable && !goAvailable && !buildDocker { + fmt.Println("hover: Failed to lookup `go` executable. Please install go or add '--docker' to force running in Docker container.\nhttps://golang.org/doc/install") os.Exit(1) } flutterBin, err = exec.LookPath("flutter") diff --git a/cmd/run.go b/cmd/run.go index b56d4052..1a05b389 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -23,6 +23,7 @@ func init() { runCmd.Flags().StringVarP(&runObservatoryPort, "observatory-port", "", "50300", "The observatory port used to connect hover to VM services (hot-reload/debug/..)") runCmd.Flags().BoolVar(&buildOmitEmbedder, "omit-embedder", false, "Don't (re)compile 'go-flutter' source code, useful when only working with Dart code") runCmd.Flags().BoolVar(&buildOmitFlutterBundle, "omit-flutter", false, "Don't (re)compile the current Flutter project, useful when only working with Golang code (plugin)") + runCmd.PersistentFlags().BoolVar(&buildDocker, "docker", false, "Compile and run in Docker container only. No need to install go") rootCmd.AddCommand(runCmd) }