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  }