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 }