github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/pkg/commands/build/packager.go (about) 1 package build 2 3 import ( 4 "bytes" 5 "fmt" 6 "image" 7 "os" 8 "path/filepath" 9 10 "github.com/AlpineAIO/wails/v2/internal/project" 11 "github.com/leaanthony/winicon" 12 "github.com/tc-hib/winres" 13 "github.com/tc-hib/winres/version" 14 15 "github.com/AlpineAIO/wails/v2/pkg/buildassets" 16 "github.com/jackmordaunt/icns" 17 "github.com/pkg/errors" 18 19 "github.com/AlpineAIO/wails/v2/internal/fs" 20 ) 21 22 // PackageProject packages the application 23 func packageProject(options *Options, platform string) error { 24 var err error 25 switch platform { 26 case "darwin": 27 err = packageApplicationForDarwin(options) 28 case "windows": 29 err = packageApplicationForWindows(options) 30 case "linux": 31 err = packageApplicationForLinux(options) 32 default: 33 err = fmt.Errorf("packing not supported for %s yet", platform) 34 } 35 36 if err != nil { 37 return err 38 } 39 40 return nil 41 } 42 43 // cleanBinDirectory will remove an existing bin directory and recreate it 44 func cleanBinDirectory(options *Options) error { 45 buildDirectory := options.BinDirectory 46 47 // Clear out old builds 48 if fs.DirExists(buildDirectory) { 49 err := os.RemoveAll(buildDirectory) 50 if err != nil { 51 return err 52 } 53 } 54 55 // Create clean directory 56 err := os.MkdirAll(buildDirectory, 0o700) 57 if err != nil { 58 return err 59 } 60 61 return nil 62 } 63 64 func packageApplicationForDarwin(options *Options) error { 65 var err error 66 67 // Create directory structure 68 bundlename := options.BundleName 69 if bundlename == "" { 70 bundlename = options.ProjectData.Name + ".app" 71 } 72 73 contentsDirectory := filepath.Join(options.BinDirectory, bundlename, "/Contents") 74 exeDir := filepath.Join(contentsDirectory, "/MacOS") 75 err = fs.MkDirs(exeDir, 0o755) 76 if err != nil { 77 return err 78 } 79 resourceDir := filepath.Join(contentsDirectory, "/Resources") 80 err = fs.MkDirs(resourceDir, 0o755) 81 if err != nil { 82 return err 83 } 84 // Copy binary 85 packedBinaryPath := filepath.Join(exeDir, options.ProjectData.Name) 86 err = fs.MoveFile(options.CompiledBinary, packedBinaryPath) 87 if err != nil { 88 return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename) 89 } 90 91 // Generate Info.plist 92 err = processPList(options, contentsDirectory) 93 if err != nil { 94 return err 95 } 96 97 // Generate App Icon 98 err = processDarwinIcon(options.ProjectData, "appicon", resourceDir, "iconfile") 99 if err != nil { 100 return err 101 } 102 103 // Generate FileAssociation Icons 104 for _, fileAssociation := range options.ProjectData.Info.FileAssociations { 105 err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir, "") 106 if err != nil { 107 return err 108 } 109 } 110 111 options.CompiledBinary = packedBinaryPath 112 113 return nil 114 } 115 116 func processPList(options *Options, contentsDirectory string) error { 117 sourcePList := "Info.plist" 118 if options.Mode == Dev { 119 // Use Info.dev.plist if using build mode 120 sourcePList = "Info.dev.plist" 121 } 122 123 // Read the resolved BuildAssets file and copy it to the destination 124 content, err := buildassets.ReadFileWithProjectData(options.ProjectData, "darwin/"+sourcePList) 125 if err != nil { 126 return err 127 } 128 129 targetFile := filepath.Join(contentsDirectory, "Info.plist") 130 return os.WriteFile(targetFile, content, 0o644) 131 } 132 133 func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string, destIconName string) (err error) { 134 appIcon, err := buildassets.ReadFile(projectData, iconName+".png") 135 if err != nil { 136 return err 137 } 138 139 srcImg, _, err := image.Decode(bytes.NewBuffer(appIcon)) 140 if err != nil { 141 return err 142 } 143 144 if destIconName == "" { 145 destIconName = iconName 146 } 147 148 tgtBundle := filepath.Join(resourceDir, destIconName+".icns") 149 dest, err := os.Create(tgtBundle) 150 if err != nil { 151 return err 152 } 153 defer func() { 154 err = dest.Close() 155 if err == nil { 156 return 157 } 158 }() 159 return icns.Encode(dest, srcImg) 160 } 161 162 func packageApplicationForWindows(options *Options) error { 163 // Generate app icon 164 var err error 165 err = generateIcoFile(options, "appicon", "icon") 166 if err != nil { 167 return err 168 } 169 170 // Generate FileAssociation Icons 171 for _, fileAssociation := range options.ProjectData.Info.FileAssociations { 172 err = generateIcoFile(options, fileAssociation.IconName, "") 173 if err != nil { 174 return err 175 } 176 } 177 178 // Create syso file 179 err = compileResources(options) 180 if err != nil { 181 return err 182 } 183 184 return nil 185 } 186 187 func packageApplicationForLinux(_ *Options) error { 188 return nil 189 } 190 191 func generateIcoFile(options *Options, iconName string, destIconName string) error { 192 content, err := buildassets.ReadFile(options.ProjectData, iconName+".png") 193 if err != nil { 194 return err 195 } 196 197 if destIconName == "" { 198 destIconName = iconName 199 } 200 201 // Check ico file exists already 202 icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+destIconName+".ico") 203 if !fs.FileExists(icoFile) { 204 if dir := filepath.Dir(icoFile); !fs.DirExists(dir) { 205 if err := fs.MkDirs(dir, 0o755); err != nil { 206 return err 207 } 208 } 209 210 output, err := os.OpenFile(icoFile, os.O_CREATE|os.O_WRONLY, 0o644) 211 if err != nil { 212 return err 213 } 214 defer output.Close() 215 216 err = winicon.GenerateIcon(bytes.NewBuffer(content), output, []int{256, 128, 64, 48, 32, 16}) 217 if err != nil { 218 return err 219 } 220 } 221 return nil 222 } 223 224 func compileResources(options *Options) error { 225 currentDir, err := os.Getwd() 226 if err != nil { 227 return err 228 } 229 defer func() { 230 _ = os.Chdir(currentDir) 231 }() 232 windowsDir := filepath.Join(options.ProjectData.GetBuildDir(), "windows") 233 err = os.Chdir(windowsDir) 234 if err != nil { 235 return err 236 } 237 rs := winres.ResourceSet{} 238 icon := filepath.Join(windowsDir, "icon.ico") 239 iconFile, err := os.Open(icon) 240 if err != nil { 241 return err 242 } 243 defer iconFile.Close() 244 ico, err := winres.LoadICO(iconFile) 245 if err != nil { 246 return fmt.Errorf("couldn't load icon from icon.ico: %w", err) 247 } 248 err = rs.SetIcon(winres.RT_ICON, ico) 249 if err != nil { 250 return err 251 } 252 253 manifestData, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/wails.exe.manifest") 254 if err != nil { 255 return err 256 } 257 258 xmlData, err := winres.AppManifestFromXML(manifestData) 259 if err != nil { 260 return err 261 } 262 rs.SetManifest(xmlData) 263 264 versionInfo, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/info.json") 265 if err != nil { 266 return err 267 } 268 269 if len(versionInfo) != 0 { 270 var v version.Info 271 if err := v.UnmarshalJSON(versionInfo); err != nil { 272 return err 273 } 274 rs.SetVersionInfo(v) 275 } 276 277 targetFile := filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso") 278 fout, err := os.Create(targetFile) 279 if err != nil { 280 return err 281 } 282 defer fout.Close() 283 284 archs := map[string]winres.Arch{ 285 "amd64": winres.ArchAMD64, 286 "arm64": winres.ArchARM64, 287 "386": winres.ArchI386, 288 } 289 targetArch, supported := archs[options.Arch] 290 if !supported { 291 return fmt.Errorf("arch '%s' not supported", options.Arch) 292 } 293 294 err = rs.WriteObject(fout, targetArch) 295 if err != nil { 296 return err 297 } 298 return nil 299 }