diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..bc0d6013e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/wails/main.go", + "env": {}, + "args": [ + "setup" + ] + } + ] +} \ No newline at end of file diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 000000000..3577d5d8d --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,268 @@ +package cmd + +import ( + "flag" + "fmt" + "os" + "strings" +) + +// NewCli - Creates a new Cli application object +func NewCli(name, description string) *Cli { + result := &Cli{} + result.rootCommand = NewCommand(name, description, result, "") + result.log = NewLogger() + return result +} + +// Cli - The main application object +type Cli struct { + rootCommand *Command + defaultCommand *Command + preRunCommand func(*Cli) error + log *Logger +} + +// Version - Set the Application version string +func (c *Cli) Version(version string) { + c.rootCommand.AppVersion = version +} + +// PrintHelp - Prints the application's help +func (c *Cli) PrintHelp() { + c.rootCommand.PrintHelp() +} + +// Run - Runs the application with the given arguments +func (c *Cli) Run(args ...string) error { + if c.preRunCommand != nil { + err := c.preRunCommand(c) + if err != nil { + return err + } + } + if len(args) == 0 { + args = os.Args[1:] + } + return c.rootCommand.Run(args) +} + +// DefaultCommand - Sets the given command as the command to run when +// no other commands given +func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli { + c.defaultCommand = defaultCommand + return c +} + +// Command - Adds a command to the application +func (c *Cli) Command(name, description string) *Command { + return c.rootCommand.Command(name, description) +} + +// PreRun - Calls the given function before running the specific command +func (c *Cli) PreRun(callback func(*Cli) error) { + c.preRunCommand = callback +} + +// BoolFlag - Adds a boolean flag to the root command +func (c *Cli) BoolFlag(name, description string, variable *bool) *Command { + c.rootCommand.BoolFlag(name, description, variable) + return c.rootCommand +} + +// StringFlag - Adds a string flag to the root command +func (c *Cli) StringFlag(name, description string, variable *string) *Command { + c.rootCommand.StringFlag(name, description, variable) + return c.rootCommand +} + +// Action represents a function that gets calls when the command is called by +// the user +type Action func() error + +// Command represents a command that may be run by the user +type Command struct { + Name string + CommandPath string + Shortdescription string + Longdescription string + AppVersion string + SubCommands []*Command + SubCommandsMap map[string]*Command + longestSubcommand int + ActionCallback Action + App *Cli + Flags *flag.FlagSet + flagCount int + log *Logger + helpFlag bool +} + +// NewCommand creates a new Command +func NewCommand(name string, description string, app *Cli, parentCommandPath string) *Command { + result := &Command{ + Name: name, + Shortdescription: description, + SubCommandsMap: make(map[string]*Command), + App: app, + } + + // Set up command path + if parentCommandPath != "" { + result.CommandPath += parentCommandPath + " " + } + result.CommandPath += name + + // Set up flag set + result.Flags = flag.NewFlagSet(result.CommandPath, flag.ContinueOnError) + result.BoolFlag("help", "Get help on the '"+result.CommandPath+"' command.", &result.helpFlag) + + // result.Flags.Usage = result.PrintHelp + + return result +} + +// parseFlags parses the given flags +func (c *Command) parseFlags(args []string) error { + // Parse flags + tmp := os.Stderr + os.Stderr = nil + err := c.Flags.Parse(args) + os.Stderr = tmp + if err != nil { + fmt.Printf("Error: %s\n\n", err.Error()) + c.PrintHelp() + } + return err +} + +// Run - Runs the Command with the given arguments +func (c *Command) Run(args []string) error { + + // If we have arguments, process them + if len(args) > 0 { + // Check for subcommand + subcommand := c.SubCommandsMap[args[0]] + if subcommand != nil { + return subcommand.Run(args[1:]) + } + + // Parse flags + err := c.parseFlags(args) + if err != nil { + fmt.Printf("Error: %s\n\n", err.Error()) + c.PrintHelp() + return err + } + + // Help takes precedence + if c.helpFlag { + c.PrintHelp() + return nil + } + } + + // Do we have an action? + if c.ActionCallback != nil { + return c.ActionCallback() + } + + // If we haven't specified a subcommand + // check for an app level default command + if c.App.defaultCommand != nil { + // Prevent recursion! + if c.App.defaultCommand != c { + return c.App.defaultCommand.Run(args) + } + } + + // Nothing left we can do + c.PrintHelp() + return nil +} + +// Action - Define an action from this command +func (c *Command) Action(callback Action) *Command { + c.ActionCallback = callback + return c +} + +// PrintHelp - Output the help text for this command +func (c *Command) PrintHelp() { + versionString := c.AppVersion + if versionString != "" { + versionString = " " + versionString + } + commandTitle := c.CommandPath + if c.Shortdescription != "" { + commandTitle += " - " + c.Shortdescription + } + // Ignore root command + if c.CommandPath != c.Name { + c.log.Yellow(commandTitle) + } + if c.Longdescription != "" { + fmt.Println() + fmt.Println(c.Longdescription + "\n") + } + if len(c.SubCommands) > 0 { + fmt.Println("") + c.log.White("Available commands:") + fmt.Println("") + for _, subcommand := range c.SubCommands { + spacer := strings.Repeat(" ", 3+c.longestSubcommand-len(subcommand.Name)) + isDefault := "" + if subcommand.isDefaultCommand() { + isDefault = "[default]" + } + fmt.Printf(" %s%s%s %s\n", subcommand.Name, spacer, subcommand.Shortdescription, isDefault) + } + } + if c.flagCount > 0 { + fmt.Println("") + c.log.White("Flags:") + fmt.Println() + c.Flags.SetOutput(os.Stdout) + c.Flags.PrintDefaults() + c.Flags.SetOutput(os.Stderr) + + } + fmt.Println() +} + +// isDefaultCommand returns true if called on the default command +func (c *Command) isDefaultCommand() bool { + return c.App.defaultCommand == c +} + +// Command - Defines a subcommand +func (c *Command) Command(name, description string) *Command { + result := NewCommand(name, description, c.App, c.CommandPath) + result.log = c.log + c.SubCommands = append(c.SubCommands, result) + c.SubCommandsMap[name] = result + if len(name) > c.longestSubcommand { + c.longestSubcommand = len(name) + } + return result +} + +// BoolFlag - Adds a boolean flag to the command +func (c *Command) BoolFlag(name, description string, variable *bool) *Command { + c.Flags.BoolVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// StringFlag - Adds a string flag to the command +func (c *Command) StringFlag(name, description string, variable *string) *Command { + c.Flags.StringVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// LongDescription - Sets the long description for the command +func (c *Command) LongDescription(Longdescription string) *Command { + c.Longdescription = Longdescription + return c +} diff --git a/cmd/fs.go b/cmd/fs.go new file mode 100644 index 000000000..8eea0d3de --- /dev/null +++ b/cmd/fs.go @@ -0,0 +1,132 @@ +package cmd + +import ( + "crypto/md5" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +// FSHelper - Wrapper struct for File System utility commands +type FSHelper struct { +} + +// NewFSHelper - Returns a new FSHelper +func NewFSHelper() *FSHelper { + result := &FSHelper{} + return result +} + +// DirExists - Returns true if the given path resolves to a directory on the filesystem +func (fs *FSHelper) DirExists(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsDir() +} + +// FileExists returns a boolean value indicating whether +// the given file exists +func (fs *FSHelper) FileExists(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + +// MkDirs creates the given nested directories. +// Returns error on failure +func (fs *FSHelper) MkDirs(fullPath string, mode ...os.FileMode) error { + var perms os.FileMode + perms = 0700 + if len(mode) == 1 { + perms = mode[0] + } + return os.MkdirAll(fullPath, perms) +} + +// CopyFile from source to target +func (fs *FSHelper) CopyFile(source, target string) error { + s, err := os.Open(source) + if err != nil { + return err + } + defer s.Close() + d, err := os.Create(target) + if err != nil { + return err + } + if _, err := io.Copy(d, s); err != nil { + d.Close() + return err + } + return d.Close() +} + +// Cwd returns the current working directory +// Aborts on Failure +func (fs *FSHelper) Cwd() string { + cwd, err := os.Getwd() + if err != nil { + log.Fatal("Unable to get working directory!") + } + return cwd +} + +// GetSubdirs will return a list of FQPs to subdirectories in the given directory +func (fs *FSHelper) GetSubdirs(dir string) (map[string]string, error) { + + // Read in the directory information + fileInfo, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + // Allocate space for the list + subdirs := make(map[string]string) + + // Pull out the directories and store in the map as + // map["directoryName"] = "path/to/directoryName" + for _, file := range fileInfo { + if file.IsDir() { + subdirs[file.Name()] = filepath.Join(dir, file.Name()) + } + } + return subdirs, nil +} + +// MkDir creates the given directory. +// Returns error on failure +func (fs *FSHelper) MkDir(dir string) error { + return os.Mkdir(dir, 0700) +} + +// LoadAsString will attempt to load the given file and return +// its contents as a string +func (fs *FSHelper) LoadAsString(filename string) (string, error) { + bytes, err := ioutil.ReadFile(filename) + return string(bytes), err +} + +// FileMD5 returns the md5sum of the given file +func (fs *FSHelper) FileMD5(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", err + } + defer f.Close() + + h := md5.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/cmd/linux.go b/cmd/linux.go new file mode 100644 index 000000000..2f033f41a --- /dev/null +++ b/cmd/linux.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + "strings" +) + +// LinuxDistribution is of type int +type LinuxDistribution int + +const ( + // Unknown is the catch-all distro + Unknown LinuxDistribution = 0 + // Ubuntu distribution + Ubuntu LinuxDistribution = 1 +) + +// DistroInfo contains all the information relating to a linux distribution +type DistroInfo struct { + Distribution LinuxDistribution + Description string + Release string + Codename string + DistributorID string +} + +// GetLinuxDistroInfo returns information about the running linux distribution +func GetLinuxDistroInfo() *DistroInfo { + result := &DistroInfo{Distribution: Unknown} + program := NewProgramHelper() + // Does lsb_release exist? + + lsbRelease := program.FindProgram("lsb_release") + if lsbRelease != nil { + stdout, _, _, err := lsbRelease.Run("-a") + if err != nil { + return result + } + + for _, line := range strings.Split(stdout, "\n") { + if strings.Contains(line, ":") { + // Iterate lines a + details := strings.Split(line, ":") + key := strings.TrimSpace(details[0]) + value := strings.TrimSpace(details[1]) + switch key { + case "Distributor ID": + result.DistributorID = value + switch value { + case "Ubuntu": + result.Distribution = Ubuntu + } + case "Description": + result.Description = value + case "Release": + result.Release = value + case "Codename": + result.Codename = value + + } + } + } + + } + return result +} + +// DpkgInstalled uses dpkg to see if a package is installed +func DpkgInstalled(packageName string) (bool, error) { + result := false + program := NewProgramHelper() + dpkg := program.FindProgram("dpkg") + if dpkg == nil { + return false, fmt.Errorf("cannot check dependencies: dpkg not found") + } + _, _, exitCode, _ := dpkg.Run("-L", packageName) + result = exitCode == 0 + return result, nil +} diff --git a/cmd/log.go b/cmd/log.go new file mode 100644 index 000000000..3da1f7d2e --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/fatih/color" +) + +// Logger struct +type Logger struct { +} + +// NewLogger creates a new logger! +func NewLogger() *Logger { + return &Logger{} +} + +// Yellow - Outputs yellow text +func (l *Logger) Yellow(format string, a ...interface{}) { + color.New(color.FgHiYellow).PrintfFunc()(format+"\n", a...) +} + +// Yellowf - Outputs yellow text without the newline +func (l *Logger) Yellowf(format string, a ...interface{}) { + color.New(color.FgHiYellow).PrintfFunc()(format, a...) +} + +// Green - Outputs Green text +func (l *Logger) Green(format string, a ...interface{}) { + color.New(color.FgHiGreen).PrintfFunc()(format+"\n", a...) +} + +// White - Outputs White text +func (l *Logger) White(format string, a ...interface{}) { + color.New(color.FgHiWhite).PrintfFunc()(format+"\n", a...) +} + +// WhiteUnderline - Outputs White text with underline +func (l *Logger) WhiteUnderline(format string, a ...interface{}) { + l.White(format, a...) + l.White(l.underline(format)) +} + +// YellowUnderline - Outputs Yellow text with underline +func (l *Logger) YellowUnderline(format string, a ...interface{}) { + l.Yellow(format, a...) + l.Yellow(l.underline(format)) +} + +// underline returns a string of a line, the length of the message given to it +func (l *Logger) underline(message string) string { + return strings.Repeat("-", len(message)) +} + +// Red - Outputs Red text +func (l *Logger) Red(format string, a ...interface{}) { + color.New(color.FgHiRed).PrintfFunc()(format+"\n", a...) +} + +// Error - Outputs an Error message +func (l *Logger) Error(format string, a ...interface{}) { + color.New(color.FgHiRed).PrintfFunc()("Error: "+format+"\n", a...) +} + +// PrintBanner prints the Wails banner before running commands +func (l *Logger) PrintBanner() error { + banner1 := ` _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ ` + "`" + `/ / / ___/ +| |/ |/ / /_/ / / (__ ) ` + banner2 := `|__/|__/\__,_/_/_/____/ ` + + l.Yellowf(banner1) + l.Red(Version) + l.Yellowf(banner2) + l.Green("https://wails.app") + l.White("The lightweight framework for web-like apps") + fmt.Println() + + return nil +} diff --git a/cmd/prerequisites.go b/cmd/prerequisites.go new file mode 100644 index 000000000..972ebd25f --- /dev/null +++ b/cmd/prerequisites.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "fmt" + "runtime" +) + +// Prerequisite defines a Prerequisite! +type Prerequisite struct { + Name string + Help string + Path string +} + +func newPrerequisite(name, help string) *Prerequisite { + return &Prerequisite{Name: name, Help: help} +} + +// Prerequisites is a list of things required to use Wails +type Prerequisites []*Prerequisite + +// Add given prereq object to list +func (p *Prerequisites) Add(prereq *Prerequisite) { + *p = append(*p, prereq) +} + +// GetRequiredPrograms returns a list of programs required for the platform +func GetRequiredPrograms() (*Prerequisites, error) { + switch runtime.GOOS { + case "darwin": + return getRequiredProgramsOSX(), nil + case "linux": + return getRequiredProgramsLinux(), nil + case "windows": + return getRequiredProgramsWindows(), nil + default: + return nil, fmt.Errorf("platform '%s' not supported at this time", runtime.GOOS) + } +} + +func getRequiredProgramsOSX() *Prerequisites { + result := &Prerequisites{} + result.Add(newPrerequisite("clang", "Please install with `xcode-select --install` and try again")) + result.Add(newPrerequisite("npm", "Please install from https://nodejs.org/en/download/ and try again")) + return result +} + +func getRequiredProgramsLinux() *Prerequisites { + result := &Prerequisites{} + distroInfo := GetLinuxDistroInfo() + switch distroInfo.Distribution { + case Ubuntu: + result.Add(newPrerequisite("gcc", "Please install with `sudo apt install build-essentials` and try again")) + result.Add(newPrerequisite("pkg-config", "Please install with `sudo apt install pkg-config` and try again")) + result.Add(newPrerequisite("npm", "Please install from https://nodejs.org/en/download/ and try again")) + + default: + result.Add(newPrerequisite("gcc", "Please install with your system package manager and try again")) + result.Add(newPrerequisite("pkg-config", "Please install with your system package manager and try again")) + result.Add(newPrerequisite("npm", "Please install from https://nodejs.org/en/download/ and try again")) + + } + return result +} + +// TODO: Test this on Windows +func getRequiredProgramsWindows() *Prerequisites { + result := &Prerequisites{} + result.Add(newPrerequisite("gcc", "Please install gcc from here and try again: http://tdm-gcc.tdragon.net/download. You will need to add the bin directory to your path, EG: C:\\TDM-GCC-64\\bin\\")) + result.Add(newPrerequisite("npm", "Please install node/npm from here and try again: https://nodejs.org/en/download/")) + return result +} + +// GetRequiredLibraries returns a list of libraries (packages) required for the platform +func GetRequiredLibraries() (*Prerequisites, error) { + switch runtime.GOOS { + case "darwin": + return getRequiredLibrariesOSX() + case "linux": + return getRequiredLibrariesLinux() + case "windows": + return getRequiredLibrariesWindows() + default: + return nil, fmt.Errorf("platform '%s' not supported at this time", runtime.GOOS) + } +} + +func getRequiredLibrariesOSX() (*Prerequisites, error) { + result := &Prerequisites{} + return result, nil +} + +func getRequiredLibrariesLinux() (*Prerequisites, error) { + result := &Prerequisites{} + distroInfo := GetLinuxDistroInfo() + switch distroInfo.Distribution { + case Ubuntu: + result.Add(newPrerequisite("libgtk-3-dev", "Please install with `sudo apt install libgtk-3-dev` and try again")) + result.Add(newPrerequisite("libwebkit2gtk-4.0-dev", "Please install with `sudo apt install libwebkit2gtk-4.0-dev` and try again")) + default: + result.Add(newPrerequisite("libgtk-3-dev", "Please install with your system package manager and try again")) + result.Add(newPrerequisite("libwebkit2gtk-4.0-dev", "Please install with your system package manager and try again")) + } + return result, nil +} + +func getRequiredLibrariesWindows() (*Prerequisites, error) { + result := &Prerequisites{} + return result, nil +} diff --git a/cmd/program.go b/cmd/program.go new file mode 100644 index 000000000..c92d6b382 --- /dev/null +++ b/cmd/program.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "bytes" + "os/exec" + "path/filepath" + "syscall" +) + +// ProgramHelper - Utility functions around installed applications +type ProgramHelper struct{} + +// NewProgramHelper - Creates a new ProgramHelper +func NewProgramHelper() *ProgramHelper { + return &ProgramHelper{} +} + +// IsInstalled tries to determine if the given binary name is installed +func (p *ProgramHelper) IsInstalled(programName string) bool { + _, err := exec.LookPath(programName) + return err == nil +} + +// Program - A struct to define an installed application/binary +type Program struct { + Name string `json:"name"` + Path string `json:"path"` +} + +// FindProgram attempts to find the given program on the system.FindProgram +// Returns a struct with the name and path to the program +func (p *ProgramHelper) FindProgram(programName string) *Program { + path, err := exec.LookPath(programName) + if err != nil { + return nil + } + path, err = filepath.Abs(path) + if err != nil { + return nil + } + return &Program{ + Name: programName, + Path: path, + } +} + +// GetFullPathToBinary returns the full path the the current binary +func (p *Program) GetFullPathToBinary() (string, error) { + return filepath.Abs(p.Path) +} + +// Run will execute the program with the given parameters +// Returns stdout + stderr as strings and an error if one occured +func (p *Program) Run(vars ...string) (stdout, stderr string, exitCode int, err error) { + command, err := p.GetFullPathToBinary() + if err != nil { + return "", "", 1, err + } + cmd := exec.Command(command, vars...) + var stdo, stde bytes.Buffer + cmd.Stdout = &stdo + cmd.Stderr = &stde + err = cmd.Run() + stdout = string(stdo.Bytes()) + stderr = string(stde.Bytes()) + + // https://stackoverflow.com/questions/10385551/get-exit-code-go + if err != nil { + // try to get the exit code + if exitError, ok := err.(*exec.ExitError); ok { + ws := exitError.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } else { + exitCode = 1 + if stderr == "" { + stderr = err.Error() + } + } + } else { + // success, exitCode should be 0 if go is ok + ws := cmd.ProcessState.Sys().(syscall.WaitStatus) + exitCode = ws.ExitStatus() + } + return +} diff --git a/cmd/setup.go b/cmd/setup.go new file mode 100644 index 000000000..1d619dd05 --- /dev/null +++ b/cmd/setup.go @@ -0,0 +1 @@ +package cmd diff --git a/cmd/system.go b/cmd/system.go new file mode 100644 index 000000000..c8dd6a35a --- /dev/null +++ b/cmd/system.go @@ -0,0 +1,210 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "path/filepath" + "strconv" + "time" + + "github.com/AlecAivazis/survey" + homedir "github.com/mitchellh/go-homedir" +) + +// SystemHelper - Defines everything related to the system +type SystemHelper struct { + log *Logger + fs *FSHelper + configFilename string + homeDir string + wailsSystemDir string + wailsSystemConfig string +} + +// NewSystemHelper - Creates a new System Helper +func NewSystemHelper() *SystemHelper { + result := &SystemHelper{ + fs: NewFSHelper(), + log: NewLogger(), + configFilename: "wails.json", + } + result.setSystemDirs() + return result +} + +// Internal +// setSystemDirs calculates the system directories it is interested in +func (s *SystemHelper) setSystemDirs() { + var err error + s.homeDir, err = homedir.Dir() + if err != nil { + log.Fatal("Cannot find home directory! Please file a bug report!") + } + // TODO: A better config system + s.wailsSystemDir = filepath.Join(s.homeDir, ".wails") + s.wailsSystemConfig = filepath.Join(s.wailsSystemDir, s.configFilename) +} + +// ConfigFileExists - Returns true if it does! +func (s *SystemHelper) ConfigFileExists() bool { + return s.fs.FileExists(s.wailsSystemConfig) +} + +// SystemDirExists - Returns true if it does! +func (s *SystemHelper) systemDirExists() bool { + return s.fs.DirExists(s.wailsSystemDir) +} + +// LoadConfig attempts to load the Wails system config +func (s *SystemHelper) LoadConfig() (*SystemConfig, error) { + return NewSystemConfig(s.wailsSystemConfig) +} + +// ConfigFileIsValid checks if the config file is valid +func (s *SystemHelper) ConfigFileIsValid() bool { + _, err := NewSystemConfig(s.wailsSystemConfig) + return err == nil +} + +// BackupConfig attempts to backup the system config file +func (s *SystemHelper) BackupConfig() (string, error) { + now := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + backupFilename := s.wailsSystemConfig + "." + now + err := s.fs.CopyFile(s.wailsSystemConfig, backupFilename) + if err != nil { + return "", err + } + return backupFilename, nil +} + +func (s *SystemHelper) setup() error { + + // Answers. We all need them. + answers := &SystemConfig{} + + // Try to load current values - ignore errors + config, err := s.LoadConfig() + defaultName := "" + defaultEmail := "" + if config != nil { + defaultName = config.Name + defaultEmail = config.Email + } + // Questions + var simpleQs = []*survey.Question{ + { + Name: "Name", + Prompt: &survey.Input{ + Message: "What is your name:", + Default: defaultName, + }, + Validate: survey.Required, + }, + { + Name: "Email", + Prompt: &survey.Input{ + Message: "What is your email address:", + Default: defaultEmail, + }, + Validate: survey.Required, + }, + } + + // ask the questions + err = survey.Ask(simpleQs, answers) + if err != nil { + return err + } + + // Create the directory + err = s.fs.MkDirs(s.wailsSystemDir) + if err != nil { + return err + } + + fmt.Println() + s.log.White("Wails config saved to: " + s.wailsSystemConfig) + s.log.White("Feel free to customise these settings.") + fmt.Println() + + return answers.Save(s.wailsSystemConfig) +} + +const introText = ` +Wails is a lightweight framework for creating web-like desktop apps in Go. +I'll need to ask you a few questions so I can fill in your project templates and then I will try and see if you have the correct dependencies installed. If you don't have the right tools installed, I'll try and suggest how to install them. +` + +// Initialise attempts to set up the Wails system. +// An error is returns if there is a problem +func (s *SystemHelper) Initialise() error { + + // System dir doesn't exist + if !s.systemDirExists() { + s.log.Green("Welcome to Wails!") + s.log.Green(introText) + return s.setup() + } + + // Config doesn't exist + if !s.ConfigFileExists() { + s.log.Green("Looks like the system config is missing.") + s.log.Green("To get you back on track, I'll need to ask you a few things...") + return s.setup() + } + + // Config exists but isn't valid. + if !s.ConfigFileIsValid() { + s.log.Green("Looks like the system config got corrupted.") + backupFile, err := s.BackupConfig() + if err != nil { + s.log.Green("I tried to backup your config file but got this error: %s", err.Error()) + } else { + s.log.Green("Just in case you needed it, I backed up your config file here: %s", backupFile) + } + s.log.Green("To get you back on track, I'll need to ask you a few things...") + return s.setup() + } + + return s.setup() +} + +// SystemConfig - Defines system wode configuration data +type SystemConfig struct { + Name string `json:"name"` + Email string `json:"email"` +} + +// NewSystemConfig - Creates a new SystemConfig helper object +func NewSystemConfig(filename string) (*SystemConfig, error) { + result := &SystemConfig{} + err := result.load(filename) + return result, err +} + +// Save - Saves the system config to the given filename +func (sc *SystemConfig) Save(filename string) error { + // Convert config to JSON string + theJSON, err := json.MarshalIndent(sc, "", " ") + if err != nil { + return err + } + + // Write it out to the config file + return ioutil.WriteFile(filename, theJSON, 0644) +} + +func (sc *SystemConfig) load(filename string) error { + configData, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + // Load and unmarshall! + err = json.Unmarshal(configData, &sc) + if err != nil { + return err + } + return nil +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 000000000..31f1d5364 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,5 @@ +package cmd + +// Version - Wails version +// ...oO(There must be a better way) +const Version = "v0.5 Alpha" diff --git a/cmd/wails/0_setup.go b/cmd/wails/0_setup.go new file mode 100644 index 000000000..790c646a7 --- /dev/null +++ b/cmd/wails/0_setup.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "runtime" + + "github.com/leaanthony/spinner" + "github.com/wailsapp/wails/cmd" +) + +func init() { + + commandDescription := `Sets up your local environment to develop Wails apps.` + + setupCommand := app.Command("setup", "Setup the Wails environment"). + LongDescription(commandDescription) + + app.DefaultCommand(setupCommand) + + setupCommand.Action(func() error { + + system := cmd.NewSystemHelper() + err := system.Initialise() + if err != nil { + return err + } + + var successMessage = `Ready for take off! +Create your first project by running 'wails init'.` + if runtime.GOOS != "windows" { + successMessage = "🚀 " + successMessage + } + switch runtime.GOOS { + case "darwin": + logger.Yellow("Detected Platform: OSX") + case "windows": + logger.Yellow("Detected Platform: Windows") + case "linux": + logger.Yellow("Detected Platform: Linux") + default: + return fmt.Errorf("Platform %s is currently not supported", runtime.GOOS) + } + + logger.Yellow("Checking for prerequisites...") + // Check we have a cgo capable environment + + requiredPrograms, err := cmd.GetRequiredPrograms() + if err != nil { + return err + } + errors := false + spinner := spinner.New() + programHelper := cmd.NewProgramHelper() + for _, program := range *requiredPrograms { + spinner.Start("Looking for program '%s'", program.Name) + bin := programHelper.FindProgram(program.Name) + if bin == nil { + errors = true + spinner.Errorf("Program '%s' not found. %s", program.Name, program.Help) + } else { + spinner.Successf("Program '%s' found: %s", program.Name, bin.Path) + } + } + + // Linux has library deps + if runtime.GOOS == "linux" { + // Check library prerequisites + requiredLibraries, err := cmd.GetRequiredLibraries() + if err != nil { + return err + } + distroInfo := cmd.GetLinuxDistroInfo() + for _, library := range *requiredLibraries { + spinner.Start() + switch distroInfo.Distribution { + case cmd.Ubuntu: + installed, err := cmd.DpkgInstalled(library.Name) + if err != nil { + return err + } + if !installed { + errors = true + spinner.Errorf("Library '%s' not found. %s", library.Name, library.Help) + } else { + spinner.Successf("Library '%s' installed.", library.Name) + } + default: + return fmt.Errorf("unable to check libraries on distribution '%s'. Please ensure that the '%s' equivalent is installed", distroInfo.DistributorID, library.Name) + } + } + } + + logger.White("") + + if !errors { + logger.Yellow(successMessage) + } + + return err + }) +} diff --git a/cmd/wails/main.go b/cmd/wails/main.go new file mode 100644 index 000000000..62258d618 --- /dev/null +++ b/cmd/wails/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/wailsapp/wails/cmd" +) + +// Create Logger +var logger = cmd.NewLogger() + +// Create main app +var app = cmd.NewCli("wails", "A cli tool for building Wails applications.") + +// Prints the cli banner +func printBanner(app *cmd.Cli) error { + logger.PrintBanner() + return nil +} + +// Main! +func main() { + app.PreRun(printBanner) + err := app.Run() + if err != nil { + logger.Error(err.Error()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..d171210a6 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/wailsapp/wails + +require ( + github.com/AlecAivazis/survey v1.7.1 + github.com/fatih/color v1.7.0 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/leaanthony/spinner v0.4.0 + github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0 // indirect + github.com/leaanthony/wincursor v0.0.0-20180705115120-056510f32d15 // indirect + github.com/mattn/go-colorable v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mitchellh/go-homedir v1.0.0 + gopkg.in/AlecAivazis/survey.v1 v1.7.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..1df5885c4 --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/AlecAivazis/survey v1.7.1 h1:a84v5MG2296rBkTP0e+dd4l7NxFQ69v4jzMpErkjVxc= +github.com/AlecAivazis/survey v1.7.1/go.mod h1:MVECab6WqEH1aXhj8nKIwF7HEAJAj2bhhGiSjNy3wII= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/leaanthony/spinner v0.4.0 h1:y/7FqQqqObRKYI+33bg9DGhHIY7cQHicm+Vz0Uda0Ik= +github.com/leaanthony/spinner v0.4.0/go.mod h1:2Mmv+8Brcw3NwPT1DdOLmW6+zWpSamDDFFsUvVHo2cc= +github.com/leaanthony/spinner v0.5.0 h1:OJKn+0KP6ilHxwCEOv5Lo0wPM4PgWZWLJTeUprGJK0g= +github.com/leaanthony/spinner v0.5.0/go.mod h1:2Mmv+8Brcw3NwPT1DdOLmW6+zWpSamDDFFsUvVHo2cc= +github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0 h1:1bGojw4YacLY5bqQalojiQ7mSfQbe4WIWCEgPZagowU= +github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= +github.com/leaanthony/wincursor v0.0.0-20180705115120-056510f32d15 h1:166LIty6ldcyOc7tbgfu5smsGATvEo0JZV6bnbzyEc4= +github.com/leaanthony/wincursor v0.0.0-20180705115120-056510f32d15/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/wailsapp/wails v0.0.0-20181215232634-5de8efff325d h1:lk91T4sKD98eGcaz/xC6ER+3o9Kaun7Mk8e/cNZOPMc= +gopkg.in/AlecAivazis/survey.v1 v1.7.1 h1:mzQIVyOPSXJaQWi1m6AFCjrCEPIwQBSOn48Ri8ZpzAg= +gopkg.in/AlecAivazis/survey.v1 v1.7.1/go.mod h1:2Ehl7OqkBl3Xb8VmC4oFW2bItAhnUfzIjrOzwRxCrOU=