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