github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/integrations/appinstaller/build.go (about)

     1  package appinstaller
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"encoding/xml"
     8  	"errors"
     9  	"path"
    10  	"strings"
    11  
    12  	"github.com/kubri/kubri/source"
    13  	"github.com/kubri/kubri/target"
    14  )
    15  
    16  type Config struct {
    17  	OnLaunch                  *OnLaunchConfig
    18  	AutomaticBackgroundTask   bool
    19  	ForceUpdateFromAnyVersion bool
    20  
    21  	Source         *source.Source
    22  	Target         target.Target
    23  	Version        string
    24  	Prerelease     bool
    25  	UploadPackages bool
    26  }
    27  
    28  type OnLaunchConfig struct {
    29  	HoursBetweenUpdateChecks int
    30  	ShowPrompt               bool
    31  	UpdateBlocksActivation   bool
    32  }
    33  
    34  var ErrNotValid = errors.New("not a valid bundle")
    35  
    36  func Build(ctx context.Context, c *Config) error {
    37  	releases, err := c.Source.ListReleases(ctx, &source.ListOptions{
    38  		Version:    c.Version,
    39  		Prerelease: c.Prerelease,
    40  	})
    41  	if err == source.ErrNoReleaseFound {
    42  		return nil
    43  	}
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	var r *source.Release
    49  	for _, r = range releases {
    50  		var ok bool
    51  		for _, asset := range r.Assets {
    52  			switch path.Ext(asset.Name) {
    53  			case ".msix", ".appx", ".msixbundle", ".appxbundle":
    54  				ok = true
    55  				if err := build(ctx, c, r.Version, asset); err != nil {
    56  					return err
    57  				}
    58  			}
    59  		}
    60  		if ok {
    61  			break // Only build for latest release containing supported files.
    62  		}
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  func build(ctx context.Context, c *Config, version string, asset *source.Asset) error {
    69  	b, err := c.Source.DownloadAsset(ctx, version, asset.Name)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	p, err := getPackage(b)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	if c.UploadPackages {
    80  		p.URI, err = upload(ctx, c.Target, asset.Name, b)
    81  		if err != nil {
    82  			return err
    83  		}
    84  	} else {
    85  		p.URI = asset.URL
    86  	}
    87  
    88  	res := newXML(c)
    89  	res.Version = p.Version
    90  
    91  	var name string
    92  	if strings.HasSuffix(asset.Name, "bundle") {
    93  		res.MainBundle = p
    94  		name = p.Name + ".appinstaller"
    95  	} else {
    96  		res.MainPackage = p
    97  		name = p.Name + "-" + p.ProcessorArchitecture + ".appinstaller"
    98  	}
    99  
   100  	res.URI, err = c.Target.URL(ctx, name)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	return write(ctx, c, res)
   106  }
   107  
   108  func getPackage(b []byte) (*Package, error) {
   109  	r, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	for _, f := range r.File {
   114  		if f.Name == "AppxManifest.xml" || f.Name == "AppxBundleManifest.xml" {
   115  			rc, err := f.Open()
   116  			if err != nil {
   117  				return nil, err
   118  			}
   119  			defer rc.Close()
   120  			var manifest struct{ Identity *Package }
   121  			if err = xml.NewDecoder(rc).Decode(&manifest); err != nil {
   122  				return nil, err
   123  			}
   124  			return manifest.Identity, nil
   125  		}
   126  	}
   127  	return nil, ErrNotValid
   128  }
   129  
   130  func upload(ctx context.Context, t target.Target, path string, data []byte) (string, error) {
   131  	w, err := t.NewWriter(ctx, path)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  	if _, err = w.Write(data); err != nil {
   136  		return "", err
   137  	}
   138  	if err = w.Close(); err != nil {
   139  		return "", err
   140  	}
   141  	return t.URL(ctx, path)
   142  }
   143  
   144  func newXML(c *Config) *AppInstaller {
   145  	appInstaller := &AppInstaller{}
   146  
   147  	// Get minimum required namespace.
   148  	switch {
   149  	case c.ForceUpdateFromAnyVersion,
   150  		c.OnLaunch != nil && c.OnLaunch.ShowPrompt,
   151  		c.OnLaunch != nil && c.OnLaunch.UpdateBlocksActivation:
   152  		appInstaller.XMLName.Space = "http://schemas.microsoft.com/appx/appinstaller/2018"
   153  	case c.AutomaticBackgroundTask,
   154  		c.OnLaunch != nil && c.OnLaunch.HoursBetweenUpdateChecks >= 0:
   155  		appInstaller.XMLName.Space = "http://schemas.microsoft.com/appx/appinstaller/2017/2"
   156  	default:
   157  		appInstaller.XMLName.Space = "http://schemas.microsoft.com/appx/appinstaller/2017"
   158  		return appInstaller
   159  	}
   160  
   161  	if c.OnLaunch != nil || c.AutomaticBackgroundTask || c.ForceUpdateFromAnyVersion {
   162  		appInstaller.UpdateSettings = &UpdateSettings{}
   163  		if c.AutomaticBackgroundTask {
   164  			appInstaller.UpdateSettings.AutomaticBackgroundTask = Bool(c.AutomaticBackgroundTask)
   165  		}
   166  		if c.ForceUpdateFromAnyVersion {
   167  			appInstaller.UpdateSettings.ForceUpdateFromAnyVersion = c.ForceUpdateFromAnyVersion
   168  		}
   169  		if c.OnLaunch != nil {
   170  			appInstaller.UpdateSettings.OnLaunch = (*OnLaunch)(c.OnLaunch)
   171  		}
   172  	}
   173  
   174  	return appInstaller
   175  }
   176  
   177  //nolint:gochecknoglobals
   178  var replacer = strings.NewReplacer("></Package>", " />", "></Bundle>", " />",
   179  	"></MainPackage>", " />", "></MainBundle>", " />", "></OnLaunch>", " />",
   180  	"></AutomaticBackgroundTask>", " />", "></ForceUpdateFromAnyVersion>", " />")
   181  
   182  func write(ctx context.Context, c *Config, app *AppInstaller) error {
   183  	w, err := c.Target.NewWriter(ctx, path.Base(app.URI))
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer w.Close()
   188  	if _, err = w.Write([]byte(xml.Header)); err != nil {
   189  		return err
   190  	}
   191  	b, err := xml.MarshalIndent(app, "", "\t")
   192  	if err != nil {
   193  		return err
   194  	}
   195  	if _, err = replacer.WriteString(w, string(b)); err != nil {
   196  		return err
   197  	}
   198  	return w.Close()
   199  }