github.com/secoba/wails/v2@v2.6.4/pkg/templates/templates.go (about)

     1  package templates
     2  
     3  import (
     4  	"embed"
     5  	"encoding/json"
     6  	"fmt"
     7  	gofs "io/fs"
     8  	"log"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/go-git/go-git/v5"
    15  	"github.com/go-git/go-git/v5/plumbing"
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/leaanthony/debme"
    19  	"github.com/leaanthony/gosod"
    20  	"github.com/secoba/wails/v2/internal/fs"
    21  	"github.com/secoba/wails/v2/pkg/clilogger"
    22  )
    23  
    24  //go:embed all:templates
    25  var templates embed.FS
    26  
    27  //go:embed all:ides/*
    28  var ides embed.FS
    29  
    30  //
    31  
    32  // Cahce for the templates
    33  // We use this because we need different views of the same data
    34  var templateCache []Template = nil
    35  
    36  // Data contains the data we wish to embed during template installation
    37  type Data struct {
    38  	ProjectName        string
    39  	BinaryName         string
    40  	WailsVersion       string
    41  	NPMProjectName     string
    42  	AuthorName         string
    43  	AuthorEmail        string
    44  	AuthorNameAndEmail string
    45  	WailsDirectory     string
    46  	GoSDKPath          string
    47  	WindowsFlags       string
    48  	CGOEnabled         string
    49  	OutputFile         string
    50  }
    51  
    52  // Options for installing a template
    53  type Options struct {
    54  	ProjectName         string
    55  	TemplateName        string
    56  	BinaryName          string
    57  	TargetDir           string
    58  	Logger              *clilogger.CLILogger
    59  	PathToDesktopBinary string
    60  	PathToServerBinary  string
    61  	InitGit             bool
    62  	AuthorName          string
    63  	AuthorEmail         string
    64  	IDE                 string
    65  	ProjectNameFilename string // The project name but as a valid filename
    66  	WailsVersion        string
    67  	GoSDKPath           string
    68  	WindowsFlags        string
    69  	CGOEnabled          string
    70  	CGOLDFlags          string
    71  	OutputFile          string
    72  }
    73  
    74  // Template holds data relating to a template
    75  // including the metadata stored in template.json
    76  type Template struct {
    77  	// Template details
    78  	Name        string `json:"name"`
    79  	ShortName   string `json:"shortname"`
    80  	Author      string `json:"author"`
    81  	Description string `json:"description"`
    82  	HelpURL     string `json:"helpurl"`
    83  
    84  	// Other data
    85  	FS gofs.FS `json:"-"`
    86  }
    87  
    88  func parseTemplate(template gofs.FS) (Template, error) {
    89  	var result Template
    90  	data, err := gofs.ReadFile(template, "template.json")
    91  	if err != nil {
    92  		return result, errors.Wrap(err, "Error parsing template")
    93  	}
    94  	err = json.Unmarshal(data, &result)
    95  	if err != nil {
    96  		return result, err
    97  	}
    98  	result.FS = template
    99  	return result, nil
   100  }
   101  
   102  // List returns the list of available templates
   103  func List() ([]Template, error) {
   104  	// If the cache isn't loaded, load it
   105  	if templateCache == nil {
   106  		err := loadTemplateCache()
   107  		if err != nil {
   108  			return nil, err
   109  		}
   110  	}
   111  
   112  	return templateCache, nil
   113  }
   114  
   115  // getTemplateByShortname returns the template with the given short name
   116  func getTemplateByShortname(shortname string) (Template, error) {
   117  	var result Template
   118  
   119  	// If the cache isn't loaded, load it
   120  	if templateCache == nil {
   121  		err := loadTemplateCache()
   122  		if err != nil {
   123  			return result, err
   124  		}
   125  	}
   126  
   127  	for _, template := range templateCache {
   128  		if template.ShortName == shortname {
   129  			return template, nil
   130  		}
   131  	}
   132  
   133  	return result, fmt.Errorf("shortname '%s' is not a valid template shortname", shortname)
   134  }
   135  
   136  // Loads the template cache
   137  func loadTemplateCache() error {
   138  	templatesFS, err := debme.FS(templates, "templates")
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	// Get directories
   144  	files, err := templatesFS.ReadDir(".")
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	// Reset cache
   150  	templateCache = []Template{}
   151  
   152  	for _, file := range files {
   153  		if file.IsDir() {
   154  			templateFS, err := templatesFS.FS(file.Name())
   155  			if err != nil {
   156  				return err
   157  			}
   158  			template, err := parseTemplate(templateFS)
   159  			if err != nil {
   160  				// Cannot parse this template, continue
   161  				continue
   162  			}
   163  			templateCache = append(templateCache, template)
   164  		}
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  // Install the given template. Returns true if the template is remote.
   171  func Install(options *Options) (bool, *Template, error) {
   172  	// Get cwd
   173  	cwd, err := os.Getwd()
   174  	if err != nil {
   175  		return false, nil, err
   176  	}
   177  
   178  	// Did the user want to install in current directory?
   179  	if options.TargetDir == "" {
   180  		options.TargetDir = filepath.Join(cwd, options.ProjectName)
   181  		if fs.DirExists(options.TargetDir) {
   182  			return false, nil, fmt.Errorf("cannot create project directory. Dir exists: %s", options.TargetDir)
   183  		}
   184  	} else {
   185  		// Get the absolute path of the given directory
   186  		targetDir, err := filepath.Abs(options.TargetDir)
   187  		if err != nil {
   188  			return false, nil, err
   189  		}
   190  		options.TargetDir = targetDir
   191  		if !fs.DirExists(options.TargetDir) {
   192  			err := fs.Mkdir(options.TargetDir)
   193  			if err != nil {
   194  				return false, nil, err
   195  			}
   196  		}
   197  	}
   198  
   199  	// Flag to indicate remote template
   200  	remoteTemplate := false
   201  
   202  	// Is this a shortname?
   203  	template, err := getTemplateByShortname(options.TemplateName)
   204  	if err != nil {
   205  		// Is this a filepath?
   206  		templatePath, err := filepath.Abs(options.TemplateName)
   207  		if fs.DirExists(templatePath) {
   208  			templateFS := os.DirFS(templatePath)
   209  			template, err = parseTemplate(templateFS)
   210  			if err != nil {
   211  				return false, nil, errors.Wrap(err, "Error installing template")
   212  			}
   213  		} else {
   214  			// git clone to temporary dir
   215  			tempdir, err := gitclone(options)
   216  			defer func(path string) {
   217  				err := os.RemoveAll(path)
   218  				if err != nil {
   219  					log.Fatal(err)
   220  				}
   221  			}(tempdir)
   222  			if err != nil {
   223  				return false, nil, err
   224  			}
   225  			// Remove the .git directory
   226  			err = os.RemoveAll(filepath.Join(tempdir, ".git"))
   227  			if err != nil {
   228  				return false, nil, err
   229  			}
   230  
   231  			templateFS := os.DirFS(tempdir)
   232  			template, err = parseTemplate(templateFS)
   233  			if err != nil {
   234  				return false, nil, err
   235  			}
   236  			remoteTemplate = true
   237  		}
   238  	}
   239  
   240  	// Use Gosod to install the template
   241  	installer := gosod.New(template.FS)
   242  
   243  	// Ignore template.json files
   244  	installer.IgnoreFile("template.json")
   245  
   246  	// Setup the data.
   247  	// We use the directory name for the binary name, like Go
   248  	BinaryName := filepath.Base(options.TargetDir)
   249  	NPMProjectName := strings.ToLower(strings.ReplaceAll(BinaryName, " ", ""))
   250  	localWailsDirectory := fs.RelativePath("../../../../../..")
   251  
   252  	templateData := &Data{
   253  		ProjectName:    options.ProjectName,
   254  		BinaryName:     filepath.Base(options.TargetDir),
   255  		NPMProjectName: NPMProjectName,
   256  		WailsDirectory: localWailsDirectory,
   257  		AuthorEmail:    options.AuthorEmail,
   258  		AuthorName:     options.AuthorName,
   259  		WailsVersion:   options.WailsVersion,
   260  		GoSDKPath:      options.GoSDKPath,
   261  	}
   262  
   263  	// Create a formatted name and email combo.
   264  	if options.AuthorName != "" {
   265  		templateData.AuthorNameAndEmail = options.AuthorName + " "
   266  	}
   267  	if options.AuthorEmail != "" {
   268  		templateData.AuthorNameAndEmail += "<" + options.AuthorEmail + ">"
   269  	}
   270  	templateData.AuthorNameAndEmail = strings.TrimSpace(templateData.AuthorNameAndEmail)
   271  
   272  	installer.RenameFiles(map[string]string{
   273  		"gitignore.txt": ".gitignore",
   274  	})
   275  
   276  	// Extract the template
   277  	err = installer.Extract(options.TargetDir, templateData)
   278  	if err != nil {
   279  		return false, nil, err
   280  	}
   281  
   282  	err = generateIDEFiles(options)
   283  	if err != nil {
   284  		return false, nil, err
   285  	}
   286  
   287  	return remoteTemplate, &template, nil
   288  }
   289  
   290  // Clones the given uri and returns the temporary cloned directory
   291  func gitclone(options *Options) (string, error) {
   292  	// Create temporary directory
   293  	dirname, err := os.MkdirTemp("", "wails-template-*")
   294  	if err != nil {
   295  		return "", err
   296  	}
   297  
   298  	// Parse remote template url and version number
   299  	templateInfo := strings.Split(options.TemplateName, "@")
   300  	cloneOption := &git.CloneOptions{
   301  		URL: templateInfo[0],
   302  	}
   303  	if len(templateInfo) > 1 {
   304  		cloneOption.ReferenceName = plumbing.NewTagReferenceName(templateInfo[1])
   305  	}
   306  
   307  	_, err = git.PlainClone(dirname, false, cloneOption)
   308  
   309  	return dirname, err
   310  }
   311  
   312  func generateIDEFiles(options *Options) error {
   313  	switch options.IDE {
   314  	case "vscode":
   315  		return generateVSCodeFiles(options)
   316  	case "goland":
   317  		return generateGolandFiles(options)
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  type ideOptions struct {
   324  	name         string
   325  	targetDir    string
   326  	options      *Options
   327  	renameFiles  map[string]string
   328  	ignoredFiles []string
   329  }
   330  
   331  func generateGolandFiles(options *Options) error {
   332  	ideoptions := ideOptions{
   333  		name:      "goland",
   334  		targetDir: filepath.Join(options.TargetDir, ".idea"),
   335  		options:   options,
   336  		renameFiles: map[string]string{
   337  			"projectname.iml": options.ProjectNameFilename + ".iml",
   338  			"gitignore.txt":   ".gitignore",
   339  			"name":            ".name",
   340  		},
   341  	}
   342  	if !options.InitGit {
   343  		ideoptions.ignoredFiles = []string{"vcs.xml"}
   344  	}
   345  	err := installIDEFiles(ideoptions)
   346  	if err != nil {
   347  		return errors.Wrap(err, "generating Goland IDE files")
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  func generateVSCodeFiles(options *Options) error {
   354  	ideoptions := ideOptions{
   355  		name:      "vscode",
   356  		targetDir: filepath.Join(options.TargetDir, ".vscode"),
   357  		options:   options,
   358  	}
   359  	return installIDEFiles(ideoptions)
   360  }
   361  
   362  func installIDEFiles(o ideOptions) error {
   363  	source, err := debme.FS(ides, "ides/"+o.name)
   364  	if err != nil {
   365  		return err
   366  	}
   367  
   368  	// Use gosod to install the template
   369  	installer := gosod.New(source)
   370  
   371  	if o.renameFiles != nil {
   372  		installer.RenameFiles(o.renameFiles)
   373  	}
   374  
   375  	for _, ignoreFile := range o.ignoredFiles {
   376  		installer.IgnoreFile(ignoreFile)
   377  	}
   378  
   379  	binaryName := filepath.Base(o.options.TargetDir)
   380  	o.options.WindowsFlags = ""
   381  	o.options.CGOEnabled = "1"
   382  
   383  	switch runtime.GOOS {
   384  	case "windows":
   385  		binaryName += ".exe"
   386  		o.options.WindowsFlags = " -H windowsgui"
   387  		o.options.CGOEnabled = "0"
   388  	case "darwin":
   389  		o.options.CGOLDFlags = "-framework UniformTypeIdentifiers"
   390  	}
   391  
   392  	o.options.PathToDesktopBinary = filepath.ToSlash(filepath.Join("build", "bin", binaryName))
   393  
   394  	err = installer.Extract(o.targetDir, o.options)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	return nil
   400  }