Skip to content

Initial cross-compiling support #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
217 changes: 183 additions & 34 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"runtime"
Expand All @@ -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)
Expand Down Expand Up @@ -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",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeertJohan So i'm failing on this due to wayland :)

}

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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}
18 changes: 16 additions & 2 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down