2019-02-04 18:49:56 +11:00
package cmd
import (
"fmt"
2019-02-04 21:09:56 +11:00
"os"
2019-02-05 08:06:18 +11:00
"os/exec"
2020-03-28 17:58:51 -05:00
"os/user"
2019-02-04 21:09:56 +11:00
"path/filepath"
"runtime"
2020-04-04 07:20:59 -05:00
"strconv"
2020-04-21 06:00:36 +10:00
"strings"
2019-02-05 08:06:18 +11:00
"time"
2019-02-04 18:49:56 +11:00
"github.com/leaanthony/slicer"
"github.com/leaanthony/spinner"
2021-04-02 20:44:55 +11:00
wailsruntime "github.com/wailsapp/wails/runtime"
2019-02-04 18:49:56 +11:00
)
2021-04-17 08:54:37 +10:00
const xgoVersion = "1.16.3"
2020-10-29 20:18:52 +11:00
2019-03-02 12:54:10 +11:00
var fs = NewFSHelper ()
2019-02-04 18:49:56 +11:00
// ValidateFrontendConfig checks if the frontend config is valid
func ValidateFrontendConfig ( projectOptions * ProjectOptions ) error {
if projectOptions . FrontEnd . Dir == "" {
return fmt . Errorf ( "Frontend directory not set in project.json" )
}
if projectOptions . FrontEnd . Build == "" {
return fmt . Errorf ( "Frontend build command not set in project.json" )
}
if projectOptions . FrontEnd . Install == "" {
return fmt . Errorf ( "Frontend install command not set in project.json" )
}
if projectOptions . FrontEnd . Bridge == "" {
return fmt . Errorf ( "Frontend bridge config not set in project.json" )
}
return nil
}
2019-02-05 08:11:02 +11:00
// InstallGoDependencies will run go get in the current directory
2020-02-23 06:03:00 +11:00
func InstallGoDependencies ( verbose bool ) error {
var depSpinner * spinner . Spinner
if ! verbose {
depSpinner = spinner . New ( "Ensuring Dependencies are up to date..." )
depSpinner . SetSpinSpeed ( 50 )
depSpinner . Start ()
}
2021-04-17 13:44:24 +10:00
err := NewProgramHelper ( verbose ). RunCommand ( "go mod tidy" )
2019-02-04 18:49:56 +11:00
if err != nil {
2020-02-23 06:03:00 +11:00
if ! verbose {
depSpinner . Error ()
}
2019-02-04 18:49:56 +11:00
return err
}
2020-02-23 06:03:00 +11:00
if ! verbose {
depSpinner . Success ()
}
2019-02-04 18:49:56 +11:00
return nil
}
2020-04-05 12:16:46 -05:00
func InitializeCrossCompilation ( verbose bool ) error {
2020-04-04 13:08:42 -05:00
// Check Docker
if err := CheckIfInstalled ( "docker" ); err != nil {
return err
}
2020-02-20 16:46:52 +01:00
2020-04-04 14:45:03 -05:00
var packSpinner * spinner . Spinner
2020-10-29 20:18:52 +11:00
msg := fmt . Sprintf ( "Pulling wailsapp/xgo:%s docker image... (may take a while)" , xgoVersion )
2020-04-05 12:16:46 -05:00
if ! verbose {
2020-10-29 20:18:52 +11:00
packSpinner = spinner . New ( msg )
2020-04-04 14:45:03 -05:00
packSpinner . SetSpinSpeed ( 50 )
packSpinner . Start ()
} else {
2020-10-29 20:18:52 +11:00
println ( msg )
2020-04-04 14:45:03 -05:00
}
2020-04-05 12:16:46 -05:00
err := NewProgramHelper ( verbose ). RunCommandArray ([] string { "docker" ,
2020-10-29 20:18:52 +11:00
"pull" , fmt . Sprintf ( "wailsapp/xgo:%s" , xgoVersion )})
2020-04-04 14:45:03 -05:00
if err != nil {
if packSpinner != nil {
packSpinner . Error ()
}
return err
}
if packSpinner != nil {
packSpinner . Success ()
}
2020-04-05 12:16:46 -05:00
return nil
}
2020-10-29 20:18:52 +11:00
// BuildDocker builds the project using the cross compiling wailsapp/xgo:<xgoVersion> container
2020-04-05 12:16:46 -05:00
func BuildDocker ( binaryName string , buildMode string , projectOptions * ProjectOptions ) error {
var packSpinner * spinner . Spinner
if buildMode == BuildModeBridge {
return fmt . Errorf ( "you cant serve the application in cross-compilation" )
}
// Check build directory
buildDirectory := filepath . Join ( fs . Cwd (), "build" )
if ! fs . DirExists ( buildDirectory ) {
2021-04-02 20:44:55 +11:00
err := fs . MkDir ( buildDirectory )
if err != nil {
return err
}
2020-04-05 12:16:46 -05:00
}
2020-04-04 13:08:42 -05:00
buildCommand := slicer . String ()
userid := 1000
2021-05-14 14:15:06 +10:00
currentUser , _ := user . Current ()
if i , err := strconv . Atoi ( currentUser . Uid ); err == nil {
2020-04-04 13:08:42 -05:00
userid = i
}
for _ , arg := range [] string {
"docker" ,
"run" ,
"--rm" ,
2020-04-18 13:56:46 -05:00
"-v" , fmt . Sprintf ( "%s:/build" , filepath . Join ( fs . Cwd (), "build" )),
2020-04-04 13:08:42 -05:00
"-v" , fmt . Sprintf ( "%s:/source" , fs . Cwd ()),
"-e" , fmt . Sprintf ( "LOCAL_USER_ID=%v" , userid ),
2020-10-29 20:18:52 +11:00
"-e" , fmt . Sprintf ( "FLAG_TAGS=%s" , projectOptions . Tags ),
2020-04-04 13:08:42 -05:00
"-e" , fmt . Sprintf ( "FLAG_LDFLAGS=%s" , ldFlags ( projectOptions , buildMode )),
"-e" , "FLAG_V=false" ,
"-e" , "FLAG_X=false" ,
"-e" , "FLAG_RACE=false" ,
"-e" , "FLAG_BUILDMODE=default" ,
"-e" , "FLAG_TRIMPATH=false" ,
2020-10-29 20:18:52 +11:00
"-e" , fmt . Sprintf ( "TARGETS=%s/%s" , projectOptions . Platform , projectOptions . Architecture ),
2020-04-04 13:08:42 -05:00
"-e" , "GOPROXY=" ,
"-e" , "GO111MODULE=on" ,
} {
buildCommand . Add ( arg )
}
2020-10-29 20:18:52 +11:00
if projectOptions . GoPath != "" {
buildCommand . Add ( "-v" )
buildCommand . Add ( fmt . Sprintf ( "%s:/go" , projectOptions . GoPath ))
}
buildCommand . Add ( fmt . Sprintf ( "wailsapp/xgo:%s" , xgoVersion ))
buildCommand . Add ( "." )
2020-04-04 14:45:03 -05:00
compileMessage := fmt . Sprintf (
2020-10-29 20:18:52 +11:00
"Packing + Compiling project for %s/%s using docker image wailsapp/xgo:%s" ,
projectOptions . Platform , projectOptions . Architecture , xgoVersion )
2020-04-04 13:08:42 -05:00
if buildMode == BuildModeDebug {
compileMessage += " (Debug Mode)"
}
if ! projectOptions . Verbose {
packSpinner = spinner . New ( compileMessage + "..." )
packSpinner . SetSpinSpeed ( 50 )
packSpinner . Start ()
} else {
println ( compileMessage )
}
2020-04-05 12:16:46 -05:00
err := NewProgramHelper ( projectOptions . Verbose ). RunCommandArray ( buildCommand . AsSlice ())
2020-04-04 13:08:42 -05:00
if err != nil {
if packSpinner != nil {
packSpinner . Error ()
2020-02-20 16:46:18 +01:00
}
2020-04-04 13:08:42 -05:00
return err
}
if packSpinner != nil {
packSpinner . Success ()
}
return nil
}
// BuildNative builds on the target platform itself.
func BuildNative ( binaryName string , forceRebuild bool , buildMode string , projectOptions * ProjectOptions ) error {
2020-04-05 12:16:46 -05:00
if err := CheckWindres (); err != nil {
return err
}
2019-02-04 18:49:56 +11:00
compileMessage := "Packing + Compiling project"
2019-02-16 06:58:30 +11:00
if buildMode == BuildModeDebug {
2019-02-04 18:49:56 +11:00
compileMessage += " (Debug Mode)"
}
2020-02-23 06:03:00 +11:00
var packSpinner * spinner . Spinner
if ! projectOptions . Verbose {
packSpinner = spinner . New ( compileMessage + "..." )
packSpinner . SetSpinSpeed ( 50 )
packSpinner . Start ()
} else {
println ( compileMessage )
}
2019-02-04 18:49:56 +11:00
buildCommand := slicer . String ()
2020-04-04 13:08:42 -05:00
buildCommand . Add ( "go" )
2019-03-08 20:38:55 +11:00
2020-04-06 09:44:33 -05:00
buildCommand . Add ( "build" )
2019-05-09 19:31:07 +10:00
2020-04-04 13:08:42 -05:00
if binaryName != "" {
2020-04-21 06:00:36 +10:00
// Alter binary name based on OS
switch projectOptions . Platform {
case "windows" :
if ! strings . HasSuffix ( binaryName , ".exe" ) {
binaryName += ".exe"
}
default :
if strings . HasSuffix ( binaryName , ".exe" ) {
binaryName = strings . TrimSuffix ( binaryName , ".exe" )
}
}
2020-04-04 13:08:42 -05:00
buildCommand . Add ( "-o" , filepath . Join ( "build" , binaryName ))
2019-02-04 18:49:56 +11:00
}
// If we are forcing a rebuild
2020-04-04 13:08:42 -05:00
if forceRebuild {
2019-02-04 18:49:56 +11:00
buildCommand . Add ( "-a" )
}
2020-04-04 13:08:42 -05:00
buildCommand . AddSlice ([] string { "-ldflags" , ldFlags ( projectOptions , buildMode )})
2019-02-20 22:24:47 +11:00
2020-10-29 20:18:52 +11:00
if projectOptions . Tags != "" {
buildCommand . AddSlice ([] string { "--tags" , projectOptions . Tags })
}
2020-03-28 17:58:51 -05:00
if projectOptions . Verbose {
fmt . Printf ( "Command: %v\n" , buildCommand . AsSlice ())
2020-02-19 13:44:30 +01:00
}
2020-04-04 13:28:25 -05:00
err := NewProgramHelper ( projectOptions . Verbose ). RunCommandArray ( buildCommand . AsSlice ())
2019-02-04 18:49:56 +11:00
if err != nil {
2020-02-23 06:03:00 +11:00
if packSpinner != nil {
packSpinner . Error ()
}
2019-02-04 18:49:56 +11:00
return err
}
2020-02-23 06:03:00 +11:00
if packSpinner != nil {
packSpinner . Success ()
}
2019-02-20 23:01:33 +11:00
2020-04-04 13:08:42 -05:00
return nil
}
// BuildApplication will attempt to build the project based on the given inputs
func BuildApplication ( binaryName string , forceRebuild bool , buildMode string , packageApp bool , projectOptions * ProjectOptions ) error {
var err error
2020-04-04 13:28:25 -05:00
2020-04-05 12:16:46 -05:00
if projectOptions . CrossCompile {
if err := InitializeCrossCompilation ( projectOptions . Verbose ); err != nil {
return err
}
}
helper := NewPackageHelper ( projectOptions . Platform )
// Generate windows resources
if projectOptions . Platform == "windows" {
if err := helper . PackageWindows ( projectOptions , false ); err != nil {
return err
}
}
2020-04-04 13:08:42 -05:00
if projectOptions . CrossCompile {
err = BuildDocker ( binaryName , buildMode , projectOptions )
} else {
err = BuildNative ( binaryName , forceRebuild , buildMode , projectOptions )
}
if err != nil {
return err
}
2019-03-20 08:11:46 +11:00
if packageApp {
err = PackageApplication ( projectOptions )
if err != nil {
return err
}
}
2019-02-04 18:49:56 +11:00
return nil
}
2019-05-09 19:31:07 +10:00
// PackageApplication will attempt to package the application in a platform dependent way
2019-02-04 18:49:56 +11:00
func PackageApplication ( projectOptions * ProjectOptions ) error {
2020-02-23 06:03:00 +11:00
var packageSpinner * spinner . Spinner
if projectOptions . Verbose {
2020-04-04 13:08:42 -05:00
packageSpinner = spinner . New ( "Packaging application..." )
2020-02-23 06:03:00 +11:00
packageSpinner . SetSpinSpeed ( 50 )
packageSpinner . Start ()
}
2020-04-04 13:08:42 -05:00
2020-02-19 13:44:30 +01:00
err := NewPackageHelper ( projectOptions . Platform ). Package ( projectOptions )
2019-02-04 18:49:56 +11:00
if err != nil {
2020-02-23 06:03:00 +11:00
if packageSpinner != nil {
packageSpinner . Error ()
}
2019-02-04 18:49:56 +11:00
return err
}
2020-02-23 06:03:00 +11:00
if packageSpinner != nil {
packageSpinner . Success ()
}
2019-02-04 18:49:56 +11:00
return nil
}
2019-02-05 08:11:02 +11:00
// BuildFrontend runs the given build command
2020-02-23 06:03:00 +11:00
func BuildFrontend ( projectOptions * ProjectOptions ) error {
var buildFESpinner * spinner . Spinner
if ! projectOptions . Verbose {
buildFESpinner = spinner . New ( "Building frontend..." )
buildFESpinner . SetSpinSpeed ( 50 )
buildFESpinner . Start ()
} else {
println ( "Building frontend..." )
}
err := NewProgramHelper ( projectOptions . Verbose ). RunCommand ( projectOptions . FrontEnd . Build )
2019-02-04 18:49:56 +11:00
if err != nil {
2020-02-23 06:03:00 +11:00
if buildFESpinner != nil {
buildFESpinner . Error ()
}
2019-02-04 18:49:56 +11:00
return err
}
2020-02-23 06:03:00 +11:00
if buildFESpinner != nil {
buildFESpinner . Success ()
}
2019-02-04 18:49:56 +11:00
return nil
}
2019-02-20 22:24:47 +11:00
// CheckWindres checks if Windres is installed and if not, aborts
func CheckWindres () ( err error ) {
2020-04-05 12:16:46 -05:00
if runtime . GOOS != "windows" { // FIXME: Handle windows cross-compile for windows!
2019-02-20 22:24:47 +11:00
return nil
}
programHelper := NewProgramHelper ()
if ! programHelper . IsInstalled ( "windres" ) {
2019-03-03 11:34:00 +11:00
return fmt . Errorf ( "windres not installed. It comes by default with mingw. Ensure you have installed mingw correctly" )
2019-02-20 22:24:47 +11:00
}
return nil
2020-02-20 16:46:52 +01:00
}
// CheckIfInstalled returns if application is installed
func CheckIfInstalled ( application string ) ( err error ) {
programHelper := NewProgramHelper ()
if ! programHelper . IsInstalled ( application ) {
return fmt . Errorf ( "%s not installed. Ensure you have installed %s correctly" , application , application )
}
return nil
2019-02-20 22:24:47 +11:00
}
2019-02-05 08:11:02 +11:00
// InstallFrontendDeps attempts to install the frontend dependencies based on the given options
2019-02-13 08:44:53 +11:00
func InstallFrontendDeps ( projectDir string , projectOptions * ProjectOptions , forceRebuild bool , caller string ) error {
2019-02-04 21:09:56 +11:00
// Install frontend deps
err := os . Chdir ( projectOptions . FrontEnd . Dir )
if err != nil {
return err
}
// Check if frontend deps have been updated
2020-02-23 06:03:00 +11:00
var feSpinner * spinner . Spinner
if ! projectOptions . Verbose {
feSpinner = spinner . New ( "Ensuring frontend dependencies are up to date (This may take a while)" )
feSpinner . SetSpinSpeed ( 50 )
feSpinner . Start ()
} else {
println ( "Ensuring frontend dependencies are up to date (This may take a while)" )
}
2019-02-04 21:09:56 +11:00
requiresNPMInstall := true
// Read in package.json MD5
fs := NewFSHelper ()
packageJSONMD5 , err := fs . FileMD5 ( "package.json" )
if err != nil {
return err
}
const md5sumFile = "package.json.md5"
2019-11-05 14:47:05 +11:00
// If node_modules does not exist, force a rebuild.
nodeModulesPath , err := filepath . Abs ( filepath . Join ( "." , "node_modules" ))
if err != nil {
return err
}
if ! fs . DirExists ( nodeModulesPath ) {
forceRebuild = true
}
2019-02-04 21:09:56 +11:00
// If we aren't forcing the install and the md5sum file exists
if ! forceRebuild && fs . FileExists ( md5sumFile ) {
// Yes - read contents
savedMD5sum , err := fs . LoadAsString ( md5sumFile )
// File exists
if err == nil {
// Compare md5
if savedMD5sum == packageJSONMD5 {
// Same - no need for reinstall
requiresNPMInstall = false
2020-02-23 06:03:00 +11:00
if feSpinner != nil {
feSpinner . Success ( "Skipped frontend dependencies (-f to force rebuild)" )
} else {
println ( "Skipped frontend dependencies (-f to force rebuild)" )
}
2019-02-04 21:09:56 +11:00
}
}
}
// Md5 sum package.json
// Different? Build
if requiresNPMInstall || forceRebuild {
// Install dependencies
2020-02-23 06:03:00 +11:00
err = NewProgramHelper ( projectOptions . Verbose ). RunCommand ( projectOptions . FrontEnd . Install )
2019-02-04 21:09:56 +11:00
if err != nil {
2020-02-23 06:03:00 +11:00
if feSpinner != nil {
feSpinner . Error ()
}
2019-02-04 21:09:56 +11:00
return err
}
2020-02-23 06:03:00 +11:00
if feSpinner != nil {
feSpinner . Success ()
}
2019-02-04 21:09:56 +11:00
// Update md5sum file
2021-04-02 20:44:55 +11:00
err := os . WriteFile ( md5sumFile , [] byte ( packageJSONMD5 ), 0644 )
if err != nil {
return err
}
2019-02-04 21:09:56 +11:00
}
2019-10-08 06:12:08 +11:00
// Install the runtime
2021-04-02 20:44:55 +11:00
if caller == "build" {
err = InstallProdRuntime ( projectDir , projectOptions )
} else {
err = InstallBridge ( projectDir , projectOptions )
}
2019-04-27 09:29:19 +10:00
if err != nil {
return err
}
// Build frontend
2020-02-23 06:03:00 +11:00
err = BuildFrontend ( projectOptions )
2019-04-27 09:29:19 +10:00
if err != nil {
return err
}
return nil
}
2019-10-08 06:12:08 +11:00
// InstallBridge installs the relevant bridge javascript library
func InstallBridge ( projectDir string , projectOptions * ProjectOptions ) error {
bridgeFileTarget := filepath . Join ( projectDir , projectOptions . FrontEnd . Dir , "node_modules" , "@wailsapp" , "runtime" , "init.js" )
2021-04-02 20:44:55 +11:00
err := fs . CreateFile ( bridgeFileTarget , wailsruntime . BridgeJS )
2019-10-08 06:12:08 +11:00
return err
}
// InstallProdRuntime installs the production runtime
func InstallProdRuntime ( projectDir string , projectOptions * ProjectOptions ) error {
bridgeFileTarget := filepath . Join ( projectDir , projectOptions . FrontEnd . Dir , "node_modules" , "@wailsapp" , "runtime" , "init.js" )
2021-04-02 20:44:55 +11:00
err := fs . CreateFile ( bridgeFileTarget , wailsruntime . InitJS )
2019-10-08 06:12:08 +11:00
return err
2019-02-04 21:09:56 +11:00
}
2019-02-05 08:11:02 +11:00
// ServeProject attempts to serve up the current project so that it may be connected to
// via the Wails bridge
2019-02-05 08:06:18 +11:00
func ServeProject ( projectOptions * ProjectOptions , logger * Logger ) error {
go func () {
time . Sleep ( 2 * time . Second )
2020-10-29 20:18:52 +11:00
if projectOptions . Platform == "windows" {
logger . Yellow ( "*** Please note: Windows builds use mshtml which is only compatible with IE11. We strongly recommend only using IE11 when running 'wails serve'! For more information, please read https://wails.app/guides/windows/ ***" )
}
2019-02-05 08:06:18 +11:00
logger . Green ( ">>>>> To connect, you will need to run '" + projectOptions . FrontEnd . Serve + "' in the '" + projectOptions . FrontEnd . Dir + "' directory <<<<<" )
}()
2020-04-06 09:44:33 -05:00
location , err := filepath . Abs ( filepath . Join ( "build" , projectOptions . BinaryName ))
2019-02-05 08:06:18 +11:00
if err != nil {
return err
}
logger . Yellow ( "Serving Application: " + location )
2021-05-14 14:15:06 +10:00
var args [] string
if len ( os . Args ) > 2 {
2021-05-30 11:17:33 +10:00
foundArgSep := false
for index , arg := range os . Args [ 2 :] {
if arg == "--" {
foundArgSep = true
continue
}
if foundArgSep {
args = os . Args [ index :]
break
}
}
2021-05-14 14:15:06 +10:00
logger . Yellow ( "Passing arguments: %+v" , args )
}
cmd := exec . Command ( location , args ... )
2019-02-05 08:06:18 +11:00
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
err = cmd . Run ()
if err != nil {
return err
}
return nil
}
2020-04-04 13:08:42 -05:00
func ldFlags ( po * ProjectOptions , buildMode string ) string {
// Setup ld flags
ldflags := "-w -s "
if buildMode == BuildModeDebug {
ldflags = ""
}
// Add windows flags
if po . Platform == "windows" && buildMode == BuildModeProd {
ldflags += "-H windowsgui "
}
2020-10-29 20:18:52 +11:00
if po . UseFirebug {
ldflags += "-X github.com/wailsapp/wails/lib/renderer.UseFirebug=true "
}
2020-04-04 13:08:42 -05:00
ldflags += "-X github.com/wailsapp/wails.BuildMode=" + buildMode
2020-06-22 20:11:56 +10:00
// Add additional ldflags passed in via the `ldflags` cli flag
if len ( po . LdFlags ) > 0 {
ldflags += " " + po . LdFlags
}
2020-04-04 13:08:42 -05:00
// If we wish to generate typescript
if po . typescriptDefsFilename != "" {
cwd , err := os . Getwd ()
if err == nil {
filename := filepath . Join ( cwd , po . FrontEnd . Dir , po . typescriptDefsFilename )
ldflags += " -X github.com/wailsapp/wails/lib/binding.typescriptDefinitionFilename=" + filename
}
}
return ldflags
}
2021-03-27 21:06:02 +11:00
func getGitConfigValue ( key string ) ( string , error ) {
output , err := exec . Command ( "git" , "config" , "--get" , "--null" , key ). Output ()
// When using --null git appends a null character (\u0000) to the command output
return strings . TrimRight ( string ( output ), "\u0000" ), err
}