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 }