5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 00:41:59 +08:00
wails/v2/pkg/templates/templates.go
2023-11-12 12:30:49 +11:00

399 lines
9.4 KiB
Go

package templates
import (
"embed"
"encoding/json"
"fmt"
gofs "io/fs"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/pkg/errors"
"github.com/leaanthony/debme"
"github.com/leaanthony/gosod"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
//go:embed all:templates
var templates embed.FS
//go:embed all:ides/*
var ides embed.FS
// Cahce for the templates
// We use this because we need different views of the same data
var templateCache []Template = nil
// Data contains the data we wish to embed during template installation
type Data struct {
ProjectName string
BinaryName string
WailsVersion string
NPMProjectName string
AuthorName string
AuthorEmail string
AuthorNameAndEmail string
WailsDirectory string
GoSDKPath string
WindowsFlags string
CGOEnabled string
OutputFile string
}
// Options for installing a template
type Options struct {
ProjectName string
TemplateName string
BinaryName string
TargetDir string
Logger *clilogger.CLILogger
PathToDesktopBinary string
PathToServerBinary string
InitGit bool
AuthorName string
AuthorEmail string
IDE string
ProjectNameFilename string // The project name but as a valid filename
WailsVersion string
GoSDKPath string
WindowsFlags string
CGOEnabled string
CGOLDFlags string
OutputFile string
}
// Template holds data relating to a template
// including the metadata stored in template.json
type Template struct {
// Template details
Name string `json:"name"`
ShortName string `json:"shortname"`
Author string `json:"author"`
Description string `json:"description"`
HelpURL string `json:"helpurl"`
// Other data
FS gofs.FS `json:"-"`
}
func parseTemplate(template gofs.FS) (Template, error) {
var result Template
data, err := gofs.ReadFile(template, "template.json")
if err != nil {
return result, errors.Wrap(err, "Error parsing template")
}
err = json.Unmarshal(data, &result)
if err != nil {
return result, err
}
result.FS = template
return result, nil
}
// List returns the list of available templates
func List() ([]Template, error) {
// If the cache isn't loaded, load it
if templateCache == nil {
err := loadTemplateCache()
if err != nil {
return nil, err
}
}
return templateCache, nil
}
// getTemplateByShortname returns the template with the given short name
func getTemplateByShortname(shortname string) (Template, error) {
var result Template
// If the cache isn't loaded, load it
if templateCache == nil {
err := loadTemplateCache()
if err != nil {
return result, err
}
}
for _, template := range templateCache {
if template.ShortName == shortname {
return template, nil
}
}
return result, fmt.Errorf("shortname '%s' is not a valid template shortname", shortname)
}
// Loads the template cache
func loadTemplateCache() error {
templatesFS, err := debme.FS(templates, "templates")
if err != nil {
return err
}
// Get directories
files, err := templatesFS.ReadDir(".")
if err != nil {
return err
}
// Reset cache
templateCache = []Template{}
for _, file := range files {
if file.IsDir() {
templateFS, err := templatesFS.FS(file.Name())
if err != nil {
return err
}
template, err := parseTemplate(templateFS)
if err != nil {
// Cannot parse this template, continue
continue
}
templateCache = append(templateCache, template)
}
}
return nil
}
// Install the given template. Returns true if the template is remote.
func Install(options *Options) (bool, *Template, error) {
// Get cwd
cwd, err := os.Getwd()
if err != nil {
return false, nil, err
}
// Did the user want to install in current directory?
if options.TargetDir == "" {
options.TargetDir = filepath.Join(cwd, options.ProjectName)
if fs.DirExists(options.TargetDir) {
return false, nil, fmt.Errorf("cannot create project directory. Dir exists: %s", options.TargetDir)
}
} else {
// Get the absolute path of the given directory
targetDir, err := filepath.Abs(options.TargetDir)
if err != nil {
return false, nil, err
}
options.TargetDir = targetDir
if !fs.DirExists(options.TargetDir) {
err := fs.Mkdir(options.TargetDir)
if err != nil {
return false, nil, err
}
}
}
// Flag to indicate remote template
remoteTemplate := false
// Is this a shortname?
template, err := getTemplateByShortname(options.TemplateName)
if err != nil {
// Is this a filepath?
templatePath, err := filepath.Abs(options.TemplateName)
if fs.DirExists(templatePath) {
templateFS := os.DirFS(templatePath)
template, err = parseTemplate(templateFS)
if err != nil {
return false, nil, errors.Wrap(err, "Error installing template")
}
} else {
// git clone to temporary dir
tempdir, err := gitclone(options)
defer func(path string) {
err := os.RemoveAll(path)
if err != nil {
log.Fatal(err)
}
}(tempdir)
if err != nil {
return false, nil, err
}
// Remove the .git directory
err = os.RemoveAll(filepath.Join(tempdir, ".git"))
if err != nil {
return false, nil, err
}
templateFS := os.DirFS(tempdir)
template, err = parseTemplate(templateFS)
if err != nil {
return false, nil, err
}
remoteTemplate = true
}
}
// Use Gosod to install the template
installer := gosod.New(template.FS)
// Ignore template.json files
installer.IgnoreFile("template.json")
// Setup the data.
// We use the directory name for the binary name, like Go
BinaryName := filepath.Base(options.TargetDir)
NPMProjectName := strings.ToLower(strings.ReplaceAll(BinaryName, " ", ""))
localWailsDirectory := fs.RelativePath("../../../../../..")
templateData := &Data{
ProjectName: options.ProjectName,
BinaryName: filepath.Base(options.TargetDir),
NPMProjectName: NPMProjectName,
WailsDirectory: localWailsDirectory,
AuthorEmail: options.AuthorEmail,
AuthorName: options.AuthorName,
WailsVersion: options.WailsVersion,
GoSDKPath: options.GoSDKPath,
}
// Create a formatted name and email combo.
if options.AuthorName != "" {
templateData.AuthorNameAndEmail = options.AuthorName + " "
}
if options.AuthorEmail != "" {
templateData.AuthorNameAndEmail += "<" + options.AuthorEmail + ">"
}
templateData.AuthorNameAndEmail = strings.TrimSpace(templateData.AuthorNameAndEmail)
installer.RenameFiles(map[string]string{
"gitignore.txt": ".gitignore",
})
// Extract the template
err = installer.Extract(options.TargetDir, templateData)
if err != nil {
return false, nil, err
}
err = generateIDEFiles(options)
if err != nil {
return false, nil, err
}
return remoteTemplate, &template, nil
}
// Clones the given uri and returns the temporary cloned directory
func gitclone(options *Options) (string, error) {
// Create temporary directory
dirname, err := os.MkdirTemp("", "wails-template-*")
if err != nil {
return "", err
}
// Parse remote template url and version number
templateInfo := strings.Split(options.TemplateName, "@")
cloneOption := &git.CloneOptions{
URL: templateInfo[0],
}
if len(templateInfo) > 1 {
cloneOption.ReferenceName = plumbing.NewTagReferenceName(templateInfo[1])
}
_, err = git.PlainClone(dirname, false, cloneOption)
return dirname, err
}
func generateIDEFiles(options *Options) error {
switch options.IDE {
case "vscode":
return generateVSCodeFiles(options)
case "goland":
return generateGolandFiles(options)
}
return nil
}
type ideOptions struct {
name string
targetDir string
options *Options
renameFiles map[string]string
ignoredFiles []string
}
func generateGolandFiles(options *Options) error {
ideoptions := ideOptions{
name: "goland",
targetDir: filepath.Join(options.TargetDir, ".idea"),
options: options,
renameFiles: map[string]string{
"projectname.iml": options.ProjectNameFilename + ".iml",
"gitignore.txt": ".gitignore",
"name": ".name",
},
}
if !options.InitGit {
ideoptions.ignoredFiles = []string{"vcs.xml"}
}
err := installIDEFiles(ideoptions)
if err != nil {
return errors.Wrap(err, "generating Goland IDE files")
}
return nil
}
func generateVSCodeFiles(options *Options) error {
ideoptions := ideOptions{
name: "vscode",
targetDir: filepath.Join(options.TargetDir, ".vscode"),
options: options,
}
return installIDEFiles(ideoptions)
}
func installIDEFiles(o ideOptions) error {
source, err := debme.FS(ides, "ides/"+o.name)
if err != nil {
return err
}
// Use gosod to install the template
installer := gosod.New(source)
if o.renameFiles != nil {
installer.RenameFiles(o.renameFiles)
}
for _, ignoreFile := range o.ignoredFiles {
installer.IgnoreFile(ignoreFile)
}
binaryName := filepath.Base(o.options.TargetDir)
o.options.WindowsFlags = ""
o.options.CGOEnabled = "1"
switch runtime.GOOS {
case "windows":
binaryName += ".exe"
o.options.WindowsFlags = " -H windowsgui"
o.options.CGOEnabled = "0"
case "darwin":
o.options.CGOLDFlags = "-framework UniformTypeIdentifiers"
}
o.options.PathToDesktopBinary = filepath.ToSlash(filepath.Join("build", "bin", binaryName))
err = installer.Extract(o.targetDir, o.options)
if err != nil {
return err
}
return nil
}