From 432fb6710a2de4c37536ea6115ffd21468d4d6b2 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Fri, 20 Mar 2026 07:14:55 -0700 Subject: [PATCH] feat: add alpha msix support (#1051) --- .github/workflows/build.yml | 41 ++- Taskfile.yml | 17 ++ acceptance_test.go | 55 ++++ cmd/nfpm/main.go | 2 +- go.mod | 3 + go.sum | 6 + internal/cmd/root.go | 5 +- msix/msix.go | 324 +++++++++++++++++++++++ msix/msix_test.go | 244 +++++++++++++++++ nfpm.go | 80 +++++- testdata/acceptance/create-test-cert.ps1 | 20 ++ testdata/acceptance/install-msix.ps1 | 40 +++ testdata/acceptance/msix.basic.yaml | 28 ++ testdata/acceptance/msix.install.yaml | 28 ++ testdata/acceptance/sign-msix.ps1 | 23 ++ testdata/acceptance/testapp/logo.png | Bin 0 -> 124 bytes testdata/acceptance/testapp/main.go | 7 + www/content/_index.md | 4 +- www/content/docs/_index.md | 2 +- www/content/docs/arch-mapping.md | 19 +- www/content/docs/cmd/nfpm.md | 4 +- www/content/docs/cmd/nfpm_package.md | 4 +- www/content/docs/configuration.md | 67 +++++ www/content/docs/quick-start.md | 2 +- 24 files changed, 1010 insertions(+), 15 deletions(-) create mode 100644 msix/msix.go create mode 100644 msix/msix_test.go create mode 100644 testdata/acceptance/create-test-cert.ps1 create mode 100644 testdata/acceptance/install-msix.ps1 create mode 100644 testdata/acceptance/msix.basic.yaml create mode 100644 testdata/acceptance/msix.install.yaml create mode 100644 testdata/acceptance/sign-msix.ps1 create mode 100644 testdata/acceptance/testapp/logo.png create mode 100644 testdata/acceptance/testapp/main.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bfca73..960e618 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,6 +91,20 @@ jobs: - run: task acceptance env: TEST_PATTERN: "/${{ matrix.pkgFormat }}/${{ matrix.pkgPlatform }}/" + msix-acceptance-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version: stable + - uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - run: task setup + - run: task acceptance + env: + TEST_PATTERN: "TestMSIXStructure" windows-build-pkgs: needs: [unit-tests] runs-on: windows-latest @@ -131,8 +145,33 @@ jobs: key: ${{ env.sha_short }} enableCrossOsArchive: true - run: task acceptance:windows:install + msix-windows-install: + needs: [unit-tests] + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version: stable + - uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Build and sign MSIX package + run: task acceptance:windows:package:msix + - name: Install MSIX package + run: task acceptance:windows:install:msix + - name: Verify installed app runs correctly + shell: pwsh + run: | + $pkg = Get-AppxPackage -Name "com.example.foo" + $exe = Join-Path $pkg.InstallLocation "app\testapp.exe" + $output = & $exe 2>&1 + if ($output -ne "nfpm-msix-test-ok") { + Write-Error "Expected 'nfpm-msix-test-ok' but got '$output'" + exit 1 + } dependabot: - needs: [license-check, unit-tests, acceptance-tests, install-windows-pkgs] + needs: [license-check, unit-tests, acceptance-tests, msix-acceptance-tests, install-windows-pkgs, msix-windows-install] runs-on: ubuntu-latest permissions: pull-requests: write diff --git a/Taskfile.yml b/Taskfile.yml index 81a4f59..734b881 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -35,12 +35,14 @@ tasks: desc: Build packages for testing vars: SRC: "./testdata/acceptance/core.complex.yaml" + MSIX_SRC: "./testdata/acceptance/msix.basic.yaml" cmds: - mkdir -p ./dist - go run ./cmd/nfpm/... pkg -f {{.SRC}} -p deb -t ./dist/foo.deb - go run ./cmd/nfpm/... pkg -f {{.SRC}} -p rpm -t ./dist/foo.rpm - go run ./cmd/nfpm/... pkg -f {{.SRC}} -p apk -t ./dist/foo.apk - go run ./cmd/nfpm/... pkg -f {{.SRC}} -p archlinux -t ./dist/foo.pkg.tar.zst + - go run ./cmd/nfpm/... pkg -f {{.MSIX_SRC}} -p msix -t ./dist/foo.msix acceptance:windows:install: desc: Install packages built with package @@ -50,6 +52,21 @@ tasks: - docker run --rm --workdir /tmp -v $PWD/dist:/tmp archlinux pacman --noconfirm -U foo.pkg.tar.zst - docker run --rm --workdir /tmp -v $PWD/dist:/tmp alpine apk add --allow-untrusted foo.apk + acceptance:windows:package:msix: + desc: Build MSIX package for Windows install testing + platforms: [windows] + cmds: + - go build -o ./dist/testapp.exe ./testdata/acceptance/testapp/ + - pwsh -ExecutionPolicy Bypass -File testdata/acceptance/create-test-cert.ps1 + - go run ./cmd/nfpm/... pkg -f ./testdata/acceptance/msix.install.yaml -p msix -t ./dist/foo.msix + - pwsh -ExecutionPolicy Bypass -File testdata/acceptance/sign-msix.ps1 + + acceptance:windows:install:msix: + desc: Install and verify MSIX package on Windows + platforms: [windows] + cmds: + - pwsh -ExecutionPolicy Bypass -File testdata/acceptance/install-msix.ps1 + acceptance:pull: desc: Pull acceptance test images vars: diff --git a/acceptance_test.go b/acceptance_test.go index 0c8703a..680b092 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -3,6 +3,8 @@ package nfpm_test import ( + "archive/zip" + "bytes" "fmt" "os" "os/exec" @@ -15,6 +17,7 @@ import ( _ "github.com/goreleaser/nfpm/v2/arch" _ "github.com/goreleaser/nfpm/v2/deb" _ "github.com/goreleaser/nfpm/v2/ipk" + _ "github.com/goreleaser/nfpm/v2/msix" _ "github.com/goreleaser/nfpm/v2/rpm" "github.com/stretchr/testify/require" ) @@ -416,3 +419,55 @@ func accept(t *testing.T, params acceptParms) { string(bts), ) } + +func TestMSIXStructure(t *testing.T) { + t.Parallel() + for _, arch := range []string{"amd64", "arm64"} { + arch := arch + t.Run(arch, func(t *testing.T) { + t.Parallel() + + configFile := "./testdata/acceptance/msix.basic.yaml" + envFunc := func(s string) string { + switch s { + case "BUILD_ARCH": + return arch + case "SEMVER": + return "v1.0.0-0.1.b1+git.abcdefgh" + default: + return os.Getenv(s) + } + } + + config, err := nfpm.ParseFileWithEnvMapping(configFile, envFunc) + require.NoError(t, err) + + info, err := config.Get("msix") + require.NoError(t, err) + require.NoError(t, nfpm.Validate(info)) + + pkg, err := nfpm.Get("msix") + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, pkg.Package(nfpm.WithDefaults(info), &buf)) + + // Open the MSIX as a ZIP archive and verify structure + reader, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + require.NoError(t, err) + + fileNames := make(map[string]bool) + for _, f := range reader.File { + fileNames[f.Name] = true + } + + // Verify required MSIX structure files exist + require.True(t, fileNames["AppxManifest.xml"], "AppxManifest.xml must exist") + require.True(t, fileNames["AppxBlockMap.xml"], "AppxBlockMap.xml must exist") + require.True(t, fileNames["[Content_Types].xml"], "[Content_Types].xml must exist") + + // Verify payload file exists + require.True(t, fileNames["app/fake.exe"], "payload file app/fake.exe must exist") + }) + } +} diff --git a/cmd/nfpm/main.go b/cmd/nfpm/main.go index d98095c..9410904 100644 --- a/cmd/nfpm/main.go +++ b/cmd/nfpm/main.go @@ -33,7 +33,7 @@ func main() { func buildVersion(version, commit, date, builtBy, treeState string) goversion.Info { return goversion.GetVersionInfo( - goversion.WithAppDetails("nfpm", "a simple and 0-dependencies apk, arch linux, deb, ipk, and rpm packager written in Go", website), + goversion.WithAppDetails("nfpm", "a simple and 0-dependencies apk, arch linux, deb, ipk, msix, and rpm packager written in Go", website), goversion.WithASCIIName(asciiArt), func(i *goversion.Info) { if commit != "" { diff --git a/go.mod b/go.mod index 58978b6..8fced63 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/ulikunitz/xz v0.5.15 + go.digitalxero.dev/go-msix v0.3.0 go.yaml.in/yaml/v3 v3.0.4 ) @@ -67,10 +68,12 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect + go.mozilla.org/pkcs7 v0.9.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index ea295bd..2371d31 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,10 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= +go.digitalxero.dev/go-msix v0.3.0 h1:fp7nTkzJK/5fwcbTszsgCnfGwBnUt0b1PGX5nYgJkfs= +go.digitalxero.dev/go-msix v0.3.0/go.mod h1:QbUpFs0AUd1zk7e9fy17suiqEAF90TR3jZY+LCI2K+c= +go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= +go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -237,3 +241,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index a58b2bb..9b30dc7 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -8,6 +8,7 @@ import ( _ "github.com/goreleaser/nfpm/v2/arch" // archlinux packager _ "github.com/goreleaser/nfpm/v2/deb" // deb packager _ "github.com/goreleaser/nfpm/v2/ipk" // ipk packager + _ "github.com/goreleaser/nfpm/v2/msix" // msix packager _ "github.com/goreleaser/nfpm/v2/rpm" // rpm packager "github.com/spf13/cobra" ) @@ -36,8 +37,8 @@ func newRootCmd(version goversion.Info, exit func(int)) *rootCmd { } cmd := &cobra.Command{ Use: "nfpm", - Short: "Packages apps on RPM, Deb, APK, Arch Linux, and ipk formats based on a YAML configuration file", - Long: `nFPM is a simple and 0-dependencies apk, arch, deb, ipk and rpm linux packager written in Go.`, + Short: "Packages apps on RPM, Deb, APK, Arch Linux, ipk, and MSIX formats based on a YAML configuration file", + Long: `nFPM is a simple and 0-dependencies apk, arch, deb, ipk, msix, and rpm packager written in Go.`, Version: version.String(), SilenceUsage: true, SilenceErrors: true, diff --git a/msix/msix.go b/msix/msix.go new file mode 100644 index 0000000..6fba2cf --- /dev/null +++ b/msix/msix.go @@ -0,0 +1,324 @@ +// Package msix implements nfpm.Packager providing .msix bindings. +package msix + +import ( + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + "go.digitalxero.dev/go-msix" +) + +const packagerName = "msix" + +// nolint: gochecknoinits +func init() { + nfpm.RegisterPackager(packagerName, Default) +} + +// nolint: gochecknoglobals +var archToMSIX = map[string]string{ + "amd64": "x64", + "x86_64": "x64", + "386": "x86", + "i386": "x86", + "i686": "x86", + "arm64": "arm64", + "aarch64": "arm64", + "arm": "arm", + "arm7": "arm", + "all": "neutral", +} + +func ensureValidArch(info *nfpm.Info) *nfpm.Info { + if info.MSIX.Arch != "" { + info.Arch = info.MSIX.Arch + } else if arch, ok := archToMSIX[info.Arch]; ok { + info.Arch = arch + } + return info +} + +// Default msix packager. +// nolint: gochecknoglobals +var Default = &MSIX{} + +// MSIX is an msix packager implementation. +type MSIX struct{} + +// ConventionalFileName returns the conventional file name for an MSIX package. +func (m *MSIX) ConventionalFileName(info *nfpm.Info) string { + info = ensureValidArch(info) + version := convertToMSIXVersion(info.Version) + return fmt.Sprintf("%s_%s_%s.msix", info.Name, version, info.Arch) +} + +// ConventionalExtension returns the file extension for MSIX packages. +func (*MSIX) ConventionalExtension() string { + return ".msix" +} + +// SetPackagerDefaults sets default values for MSIX-specific fields. +func (*MSIX) SetPackagerDefaults(info *nfpm.Info) { + needsFullTrust := false + for i := range info.MSIX.Applications { + if info.MSIX.Applications[i].EntryPoint == "" { + info.MSIX.Applications[i].EntryPoint = "Windows.FullTrustApplication" + } + if info.MSIX.Applications[i].VisualElements.BackgroundColor == "" { + info.MSIX.Applications[i].VisualElements.BackgroundColor = "transparent" + } + // Default Square150x150Logo and Square44x44Logo to the package Logo + if info.MSIX.Applications[i].VisualElements.Square150x150Logo == "" && info.MSIX.Properties.Logo != "" { + info.MSIX.Applications[i].VisualElements.Square150x150Logo = info.MSIX.Properties.Logo + } + if info.MSIX.Applications[i].VisualElements.Square44x44Logo == "" && info.MSIX.Properties.Logo != "" { + info.MSIX.Applications[i].VisualElements.Square44x44Logo = info.MSIX.Properties.Logo + } + if info.MSIX.Applications[i].EntryPoint == "Windows.FullTrustApplication" { + needsFullTrust = true + } + } + + // Auto-add runFullTrust restricted capability when any app uses FullTrustApplication + if needsFullTrust { + hasRunFullTrust := false + for _, c := range info.MSIX.Capabilities.Restricted { + if c == "runFullTrust" { + hasRunFullTrust = true + break + } + } + if !hasRunFullTrust { + info.MSIX.Capabilities.Restricted = append(info.MSIX.Capabilities.Restricted, "runFullTrust") + } + } + + if len(info.MSIX.Dependencies.TargetDeviceFamilies) == 0 { + info.MSIX.Dependencies.TargetDeviceFamilies = []nfpm.MSIXTargetDeviceFamily{ + { + Name: "Windows.Desktop", + MinVersion: "10.0.17763.0", + MaxVersionTested: "10.0.22621.0", + }, + } + } +} + +// Package writes a new MSIX package to the given writer using the given info. +func (m *MSIX) Package(info *nfpm.Info, w io.Writer) error { + m.SetPackagerDefaults(info) + info = ensureValidArch(info) + + if err := nfpm.PrepareForPackager(info, packagerName); err != nil { + return err + } + + if err := validate(info); err != nil { + return err + } + + builder := msix.NewBuilder() + + builder.Manifest = msix.Manifest{ + Identity: msix.Identity{ + Name: info.Name, + Version: convertToMSIXVersion(info.Version), + Publisher: info.MSIX.Publisher, + ProcessorArchitecture: info.Arch, + ResourceID: info.MSIX.Identity.ResourceID, + }, + Properties: buildProperties(info), + Dependencies: msix.Dependencies{ + TargetDeviceFamilies: buildTargetDeviceFamilies(info), + }, + Resources: []msix.Resource{ + {Language: "en-us"}, + }, + Applications: buildApplications(info), + Capabilities: buildCapabilities(info), + } + + if err := addContents(builder, info); err != nil { + return err + } + + if info.MSIX.Signature.PFXFile != "" { + if err := configureSigning(builder, info); err != nil { + return err + } + } + + return builder.Build(w) +} + +func validate(info *nfpm.Info) error { + if info.MSIX.Publisher == "" { + return fmt.Errorf("package %s must be provided", "msix.publisher") + } + if info.MSIX.Properties.Logo == "" { + return fmt.Errorf("package %s must be provided", "msix.properties.logo") + } + if len(info.MSIX.Applications) == 0 { + return fmt.Errorf("package %s must be provided", "msix.applications") + } + for i, app := range info.MSIX.Applications { + if app.ID == "" { + return fmt.Errorf("package %s must be provided", fmt.Sprintf("msix.applications[%d].id", i)) + } + if app.Executable == "" { + return fmt.Errorf("package %s must be provided", fmt.Sprintf("msix.applications[%d].executable", i)) + } + } + return nil +} + +func buildProperties(info *nfpm.Info) msix.Properties { + props := msix.Properties{ + DisplayName: info.MSIX.Properties.DisplayName, + PublisherDisplayName: info.MSIX.Properties.PublisherDisplayName, + Logo: info.MSIX.Properties.Logo, + Description: info.Description, + } + if props.DisplayName == "" { + props.DisplayName = info.Name + } + if props.PublisherDisplayName == "" { + props.PublisherDisplayName = info.Name + } + return props +} + +func buildTargetDeviceFamilies(info *nfpm.Info) []msix.TargetDeviceFamily { + families := make([]msix.TargetDeviceFamily, len(info.MSIX.Dependencies.TargetDeviceFamilies)) + for i, f := range info.MSIX.Dependencies.TargetDeviceFamilies { + families[i] = msix.TargetDeviceFamily{ + Name: f.Name, + MinVersion: f.MinVersion, + MaxVersionTested: f.MaxVersionTested, + } + } + return families +} + +func buildApplications(info *nfpm.Info) []msix.Application { + apps := make([]msix.Application, len(info.MSIX.Applications)) + for i, app := range info.MSIX.Applications { + apps[i] = msix.Application{ + ID: app.ID, + Executable: app.Executable, + EntryPoint: app.EntryPoint, + VisualElements: msix.VisualElements{ + DisplayName: app.VisualElements.DisplayName, + Description: app.VisualElements.Description, + BackgroundColor: app.VisualElements.BackgroundColor, + Square150x150Logo: app.VisualElements.Square150x150Logo, + Square44x44Logo: app.VisualElements.Square44x44Logo, + }, + } + if apps[i].VisualElements.DisplayName == "" { + apps[i].VisualElements.DisplayName = info.Name + } + if apps[i].VisualElements.Description == "" { + apps[i].VisualElements.Description = info.Description + } + } + return apps +} + +func buildCapabilities(info *nfpm.Info) msix.Capabilities { + caps := msix.Capabilities{} + for _, c := range info.MSIX.Capabilities.Capabilities { + caps.Capabilities = append(caps.Capabilities, msix.Capability{Name: c}) + } + for _, c := range info.MSIX.Capabilities.DeviceCapabilities { + caps.DeviceCapabilities = append(caps.DeviceCapabilities, msix.DeviceCapability{Name: c}) + } + for _, c := range info.MSIX.Capabilities.Restricted { + caps.Restricted = append(caps.Restricted, msix.RestrictedCapability{Name: c}) + } + return caps +} + +func addContents(builder *msix.Builder, info *nfpm.Info) error { + for _, content := range info.Contents { + switch content.Type { + case files.TypeDir: + // Directories are implicit in MSIX — skip + continue + case files.TypeSymlink: + log.Printf("warning: msix does not support symlinks, skipping %s", content.Destination) + continue + default: + // Treat everything else (TypeFile, TypeConfig, etc.) as regular files + dest := normalizePathForMSIX(content.Destination) + if content.Source != "" { + if err := builder.AddFile(dest, content.Source); err != nil { + return fmt.Errorf("adding file %s: %w", content.Source, err) + } + } + } + } + return nil +} + +func configureSigning(builder *msix.Builder, info *nfpm.Info) error { + pfxPath := info.MSIX.Signature.PFXFile + if _, err := os.Stat(pfxPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("PFX file not found: %w", err) + } + return fmt.Errorf("unable to access PFX file: %w", err) + } + + cert, key, chain, err := msix.LoadPFX(pfxPath, info.MSIX.Signature.KeyPassphrase) + if err != nil { + return &nfpm.ErrSigningFailure{Err: fmt.Errorf("loading PFX: %w", err)} + } + + builder.SignOptions = &msix.SignOptions{ + Certificate: cert, + PrivateKey: key, + CertChain: chain, + } + + return nil +} + +// normalizePathForMSIX converts Unix-style paths to Windows-style package paths. +func normalizePathForMSIX(path string) string { + // Remove leading slash + path = strings.TrimPrefix(path, "/") + // Convert forward slashes — go-msix normalizes internally but be explicit + return path +} + +// convertToMSIXVersion converts a semver-style version to MSIX's 4-part format. +// MSIX requires Major.Minor.Build.Revision format. +func convertToMSIXVersion(version string) string { + version = strings.TrimPrefix(version, "v") + + // Split on dots + parts := strings.SplitN(version, ".", 4) + + // Ensure all parts are valid numbers, default to 0 + result := make([]string, 4) + for i := range 4 { + if i < len(parts) { + if _, err := strconv.Atoi(parts[i]); err == nil { + result[i] = parts[i] + } else { + result[i] = "0" + } + } else { + result[i] = "0" + } + } + + return strings.Join(result, ".") +} diff --git a/msix/msix_test.go b/msix/msix_test.go new file mode 100644 index 0000000..65a60ec --- /dev/null +++ b/msix/msix_test.go @@ -0,0 +1,244 @@ +package msix + +import ( + "bytes" + "testing" + + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + "github.com/stretchr/testify/require" +) + +func exampleInfo() *nfpm.Info { + return nfpm.WithDefaults(&nfpm.Info{ + Name: "MyCompany.TestApp", + Arch: "amd64", + Description: "Test application", + Version: "v1.0.0", + Maintainer: "Test ", + Vendor: "TestCo", + Homepage: "https://example.com", + Overridables: nfpm.Overridables{ + Contents: []*files.Content{ + { + Source: "../testdata/fake", + Destination: "/app/fake.exe", + }, + { + Source: "../testdata/whatever.conf", + Destination: "/app/config.conf", + }, + }, + MSIX: nfpm.MSIX{ + Publisher: "CN=TestCompany, O=TestCompany, C=US", + Properties: nfpm.MSIXProperties{ + Logo: "app/fake.exe", + }, + Applications: []nfpm.MSIXApplication{ + { + ID: "App", + Executable: "app/fake.exe", + EntryPoint: "Windows.FullTrustApplication", + }, + }, + }, + }, + }) +} + +func TestConventionalExtension(t *testing.T) { + require.Equal(t, ".msix", Default.ConventionalExtension()) +} + +func TestConventionalFileName(t *testing.T) { + info := exampleInfo() + name := Default.ConventionalFileName(info) + require.Equal(t, "MyCompany.TestApp_1.0.0.0_x64.msix", name) +} + +func TestArchMapping(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"amd64", "x64"}, + {"x86_64", "x64"}, + {"386", "x86"}, + {"i386", "x86"}, + {"i686", "x86"}, + {"arm64", "arm64"}, + {"aarch64", "arm64"}, + {"arm", "arm"}, + {"arm7", "arm"}, + {"all", "neutral"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + info := exampleInfo() + info.Arch = tt.input + info.MSIX.Arch = "" + info = ensureValidArch(info) + require.Equal(t, tt.expected, info.Arch) + }) + } +} + +func TestArchOverride(t *testing.T) { + info := exampleInfo() + info.Arch = "amd64" + info.MSIX.Arch = "x86" + info = ensureValidArch(info) + require.Equal(t, "x86", info.Arch) +} + +func TestVersionConversion(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"1.2.3", "1.2.3.0"}, + {"v1.2.3", "1.2.3.0"}, + {"1.0.0", "1.0.0.0"}, + {"2.5", "2.5.0.0"}, + {"1", "1.0.0.0"}, + {"1.2.3.4", "1.2.3.4"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := convertToMSIXVersion(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestPackageMinimal(t *testing.T) { + info := exampleInfo() + var buf bytes.Buffer + require.NoError(t, Default.Package(info, &buf)) + require.Positive(t, buf.Len(), "package should not be empty") +} + +func TestPackageWithContents(t *testing.T) { + info := exampleInfo() + info.Contents = append(info.Contents, &files.Content{ + Source: "../testdata/whatever.conf", + Destination: "/app/extra.txt", + }) + var buf bytes.Buffer + require.NoError(t, Default.Package(info, &buf)) + require.Positive(t, buf.Len()) +} + +func TestNoInfo(t *testing.T) { + info := nfpm.WithDefaults(&nfpm.Info{}) + var buf bytes.Buffer + err := Default.Package(info, &buf) + require.Error(t, err) +} + +func TestMissingPublisher(t *testing.T) { + info := exampleInfo() + info.MSIX.Publisher = "" + var buf bytes.Buffer + err := Default.Package(info, &buf) + require.Error(t, err) + require.Contains(t, err.Error(), "msix.publisher") +} + +func TestMissingLogo(t *testing.T) { + info := exampleInfo() + info.MSIX.Properties.Logo = "" + var buf bytes.Buffer + err := Default.Package(info, &buf) + require.Error(t, err) + require.Contains(t, err.Error(), "msix.properties.logo") +} + +func TestMissingApplications(t *testing.T) { + info := exampleInfo() + info.MSIX.Applications = nil + var buf bytes.Buffer + err := Default.Package(info, &buf) + require.Error(t, err) + require.Contains(t, err.Error(), "msix.applications") +} + +func TestMissingApplicationID(t *testing.T) { + info := exampleInfo() + info.MSIX.Applications[0].ID = "" + var buf bytes.Buffer + err := Default.Package(info, &buf) + require.Error(t, err) + require.Contains(t, err.Error(), "msix.applications[0].id") +} + +func TestMissingApplicationExecutable(t *testing.T) { + info := exampleInfo() + info.MSIX.Applications[0].Executable = "" + var buf bytes.Buffer + err := Default.Package(info, &buf) + require.Error(t, err) + require.Contains(t, err.Error(), "msix.applications[0].executable") +} + +func TestSetPackagerDefaults(t *testing.T) { + info := exampleInfo() + info.MSIX.Applications[0].EntryPoint = "" + info.MSIX.Applications[0].VisualElements.BackgroundColor = "" + info.MSIX.Applications[0].VisualElements.Square150x150Logo = "" + info.MSIX.Applications[0].VisualElements.Square44x44Logo = "" + info.MSIX.Dependencies.TargetDeviceFamilies = nil + info.MSIX.Capabilities.Restricted = nil + + Default.SetPackagerDefaults(info) + + require.Equal(t, "Windows.FullTrustApplication", info.MSIX.Applications[0].EntryPoint) + require.Equal(t, "transparent", info.MSIX.Applications[0].VisualElements.BackgroundColor) + require.Equal(t, info.MSIX.Properties.Logo, info.MSIX.Applications[0].VisualElements.Square150x150Logo) + require.Equal(t, info.MSIX.Properties.Logo, info.MSIX.Applications[0].VisualElements.Square44x44Logo) + require.Len(t, info.MSIX.Dependencies.TargetDeviceFamilies, 1) + require.Equal(t, "Windows.Desktop", info.MSIX.Dependencies.TargetDeviceFamilies[0].Name) + require.Contains(t, info.MSIX.Capabilities.Restricted, "runFullTrust") +} + +func TestPackageWithCustomProperties(t *testing.T) { + info := exampleInfo() + info.MSIX.Properties = nfpm.MSIXProperties{ + DisplayName: "My Custom App", + PublisherDisplayName: "My Company", + Logo: "Assets/logo.png", + } + var buf bytes.Buffer + require.NoError(t, Default.Package(info, &buf)) + require.Positive(t, buf.Len()) +} + +func TestPackageWithCapabilities(t *testing.T) { + info := exampleInfo() + info.MSIX.Capabilities = nfpm.MSIXCapabilities{ + Capabilities: []string{"internetClient"}, + DeviceCapabilities: []string{"microphone"}, + Restricted: []string{"broadFileSystemAccess"}, + } + var buf bytes.Buffer + require.NoError(t, Default.Package(info, &buf)) + require.Positive(t, buf.Len()) +} + +func TestPackageWithDependencies(t *testing.T) { + info := exampleInfo() + info.MSIX.Dependencies = nfpm.MSIXDependencies{ + TargetDeviceFamilies: []nfpm.MSIXTargetDeviceFamily{ + { + Name: "Windows.Desktop", + MinVersion: "10.0.19041.0", + MaxVersionTested: "10.0.22621.0", + }, + }, + } + var buf bytes.Buffer + require.NoError(t, Default.Package(info, &buf)) + require.Positive(t, buf.Len()) +} diff --git a/nfpm.go b/nfpm.go index ccd05bb..7ba18b5 100644 --- a/nfpm.go +++ b/nfpm.go @@ -132,7 +132,7 @@ type PackagerWithExtension interface { // Config contains the top level configuration for packages. type Config struct { Info `yaml:",inline" json:",inline"` - Overrides map[string]*Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager,enum=apk,enum=deb,enum=rpm"` + Overrides map[string]*Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager"` envMappingFunc func(string) string } @@ -280,6 +280,14 @@ func (c *Config) expandEnvVars() { // RPM specific c.RPM.Packager = os.Expand(c.RPM.Packager, c.envMappingFunc) + + // MSIX specific + c.MSIX.Signature.PFXFile = os.Expand(c.MSIX.Signature.PFXFile, c.envMappingFunc) + c.MSIX.Publisher = os.Expand(c.MSIX.Publisher, c.envMappingFunc) + msixPassphrase := os.Expand("$NFPM_MSIX_PASSPHRASE", c.envMappingFunc) + if msixPassphrase != "" { + c.MSIX.Signature.KeyPassphrase = msixPassphrase + } } // Info contains information about a single package. @@ -361,6 +369,7 @@ type Overridables struct { APK APK `yaml:"apk,omitempty" json:"apk,omitempty" jsonschema:"title=apk-specific settings"` ArchLinux ArchLinux `yaml:"archlinux,omitempty" json:"archlinux,omitempty" jsonschema:"title=archlinux-specific settings"` IPK IPK `yaml:"ipk,omitempty" json:"ipk,omitempty" jsonschema:"title=ipk-specific settings"` + MSIX MSIX `yaml:"msix,omitempty" json:"msix,omitempty" jsonschema:"title=msix-specific settings"` } type ArchLinux struct { @@ -490,6 +499,72 @@ type IPKAlternative struct { LinkName string `yaml:"link_name,omitempty" json:"link_name,omitempty" jsonschema:"title=link name"` } +// MSIX contains configs that are only available on MSIX packages. +type MSIX struct { + Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in msix nomenclature"` + Publisher string `yaml:"publisher" json:"publisher" jsonschema:"title=publisher identity,example=CN=MyCompany\\, O=MyCompany\\, C=US"` + Identity MSIXIdentity `yaml:"identity,omitempty" json:"identity,omitempty" jsonschema:"title=package identity"` + Properties MSIXProperties `yaml:"properties,omitempty" json:"properties,omitempty" jsonschema:"title=package properties"` + Applications []MSIXApplication `yaml:"applications" json:"applications" jsonschema:"title=applications in the package"` + Dependencies MSIXDependencies `yaml:"dependencies,omitempty" json:"dependencies,omitempty" jsonschema:"title=target device families"` + Capabilities MSIXCapabilities `yaml:"capabilities,omitempty" json:"capabilities,omitempty" jsonschema:"title=package capabilities"` + Signature MSIXSignature `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=msix signature"` +} + +// MSIXIdentity contains identity fields for MSIX packages. +type MSIXIdentity struct { + ResourceID string `yaml:"resource_id,omitempty" json:"resource_id,omitempty" jsonschema:"title=resource identifier"` +} + +// MSIXProperties contains display properties for MSIX packages. +type MSIXProperties struct { + DisplayName string `yaml:"display_name,omitempty" json:"display_name,omitempty" jsonschema:"title=display name"` + PublisherDisplayName string `yaml:"publisher_display_name,omitempty" json:"publisher_display_name,omitempty" jsonschema:"title=publisher display name"` + Logo string `yaml:"logo,omitempty" json:"logo,omitempty" jsonschema:"title=package logo path"` +} + +// MSIXApplication describes an application entry in an MSIX package. +type MSIXApplication struct { + ID string `yaml:"id" json:"id" jsonschema:"title=application ID"` + Executable string `yaml:"executable" json:"executable" jsonschema:"title=executable path in package"` + EntryPoint string `yaml:"entry_point,omitempty" json:"entry_point,omitempty" jsonschema:"title=entry point,default=Windows.FullTrustApplication"` + VisualElements MSIXVisualElements `yaml:"visual_elements,omitempty" json:"visual_elements,omitempty" jsonschema:"title=visual elements"` +} + +// MSIXVisualElements contains visual presentation settings for an MSIX application. +type MSIXVisualElements struct { + DisplayName string `yaml:"display_name,omitempty" json:"display_name,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + BackgroundColor string `yaml:"background_color,omitempty" json:"background_color,omitempty" jsonschema:"default=transparent"` + Square150x150Logo string `yaml:"square150x150_logo,omitempty" json:"square150x150_logo,omitempty"` + Square44x44Logo string `yaml:"square44x44_logo,omitempty" json:"square44x44_logo,omitempty"` +} + +// MSIXDependencies contains dependency information for MSIX packages. +type MSIXDependencies struct { + TargetDeviceFamilies []MSIXTargetDeviceFamily `yaml:"target_device_families,omitempty" json:"target_device_families,omitempty"` +} + +// MSIXTargetDeviceFamily describes a target device family for an MSIX package. +type MSIXTargetDeviceFamily struct { + Name string `yaml:"name" json:"name" jsonschema:"title=device family name,example=Windows.Desktop"` + MinVersion string `yaml:"min_version" json:"min_version" jsonschema:"title=minimum OS version,example=10.0.17763.0"` + MaxVersionTested string `yaml:"max_version_tested" json:"max_version_tested" jsonschema:"title=max tested version,example=10.0.22621.0"` +} + +// MSIXCapabilities contains capability declarations for MSIX packages. +type MSIXCapabilities struct { + Capabilities []string `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` + DeviceCapabilities []string `yaml:"device_capabilities,omitempty" json:"device_capabilities,omitempty"` + Restricted []string `yaml:"restricted,omitempty" json:"restricted,omitempty"` +} + +// MSIXSignature contains signing configuration for MSIX packages. +type MSIXSignature struct { + PFXFile string `yaml:"pfx_file,omitempty" json:"pfx_file,omitempty" jsonschema:"title=PFX certificate file"` + KeyPassphrase string `yaml:"-" json:"-"` // populated from NFPM_MSIX_PASSPHRASE env var +} + // Scripts contains information about maintainer scripts for packages. type Scripts struct { PreInstall string `yaml:"preinstall,omitempty" json:"preinstall,omitempty" jsonschema:"title=pre install"` @@ -517,7 +592,8 @@ func PrepareForPackager(info *Info, packager string) (err error) { if info.Arch == "" && ((packager == "deb" && info.Deb.Arch == "") || (packager == "rpm" && info.RPM.Arch == "") || - (packager == "apk" && info.APK.Arch == "")) { + (packager == "apk" && info.APK.Arch == "") || + (packager == "msix" && info.MSIX.Arch == "")) { return ErrFieldEmpty{"arch"} } if info.Version == "" { diff --git a/testdata/acceptance/create-test-cert.ps1 b/testdata/acceptance/create-test-cert.ps1 new file mode 100644 index 0000000..fcf5f17 --- /dev/null +++ b/testdata/acceptance/create-test-cert.ps1 @@ -0,0 +1,20 @@ +$ErrorActionPreference = 'Stop' + +$cert = New-SelfSignedCertificate -Type Custom ` + -Subject 'CN=TestCompany, O=TestCompany, C=US' ` + -KeyUsage DigitalSignature ` + -FriendlyName 'nfpm-test' ` + -CertStoreLocation 'Cert:\CurrentUser\My' ` + -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}') + +Export-PfxCertificate -Cert $cert ` + -FilePath ./dist/test.pfx ` + -Password (ConvertTo-SecureString -String 'test123' -Force -AsPlainText) + +Export-Certificate -Cert $cert -FilePath ./dist/test.cer + +# Self-signed cert must be trusted as both a root CA and a publisher +Import-Certificate -FilePath ./dist/test.cer ` + -CertStoreLocation 'Cert:\LocalMachine\Root' +Import-Certificate -FilePath ./dist/test.cer ` + -CertStoreLocation 'Cert:\LocalMachine\TrustedPeople' diff --git a/testdata/acceptance/install-msix.ps1 b/testdata/acceptance/install-msix.ps1 new file mode 100644 index 0000000..1b330d8 --- /dev/null +++ b/testdata/acceptance/install-msix.ps1 @@ -0,0 +1,40 @@ +$ErrorActionPreference = 'Stop' + +# Check developer mode status +$devMode = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" -ErrorAction SilentlyContinue +Write-Host "Developer mode: AllowDevelopmentWithoutDevLicense = $($devMode.AllowDevelopmentWithoutDevLicense)" +Write-Host "Sideloading: AllowAllTrustedApps = $($devMode.AllowAllTrustedApps)" + +# Show package info before install +Write-Host "Package path: ./dist/foo.msix" +Write-Host "Package size: $((Get-Item ./dist/foo.msix).Length) bytes" + +try { + Add-AppxPackage -Path ./dist/foo.msix -Verbose + Write-Host "Package installed successfully" +} catch { + Write-Host "Install failed: $_" + Write-Host "Exception: $($_.Exception.Message)" + if ($_.Exception.InnerException) { + Write-Host "Inner: $($_.Exception.InnerException.Message)" + } + + # Try to get the event log for more details + $activityId = $_.Exception.Message -match '\[ActivityId\]\s*([a-f0-9-]+)' | Out-Null + if ($Matches) { + Write-Host "ActivityId: $($Matches[1])" + Get-AppPackageLog -ActivityID $Matches[1] | Write-Host + } + + exit 1 +} + +# Verify installation +$pkg = Get-AppxPackage -Name "com.example.foo" +if ($pkg) { + Write-Host "Verified: $($pkg.PackageFullName)" + Write-Host "InstallLocation: $($pkg.InstallLocation)" +} else { + Write-Error "Package com.example.foo not found after installation" + exit 1 +} diff --git a/testdata/acceptance/msix.basic.yaml b/testdata/acceptance/msix.basic.yaml new file mode 100644 index 0000000..c62eb2c --- /dev/null +++ b/testdata/acceptance/msix.basic.yaml @@ -0,0 +1,28 @@ +name: com.example.foo +arch: "${BUILD_ARCH}" +version: 1.2.3 +license: MIT +maintainer: "Foo Bar" +description: "A test MSIX package" +contents: + - src: ./testdata/fake + dst: /app/fake.exe + - src: ./testdata/acceptance/testapp/logo.png + dst: /Assets/logo.png +msix: + publisher: "CN=TestCompany, O=TestCompany, C=US" + properties: + logo: Assets/logo.png + applications: + - id: App + executable: app/fake.exe + entry_point: Windows.FullTrustApplication + visual_elements: + display_name: "Foo App" + description: "A test application" + background_color: transparent + dependencies: + target_device_families: + - name: Windows.Desktop + min_version: "10.0.17763.0" + max_version_tested: "10.0.22621.0" diff --git a/testdata/acceptance/msix.install.yaml b/testdata/acceptance/msix.install.yaml new file mode 100644 index 0000000..d5842fa --- /dev/null +++ b/testdata/acceptance/msix.install.yaml @@ -0,0 +1,28 @@ +name: com.example.foo +arch: amd64 +version: 1.0.0 +license: MIT +maintainer: "Test Company" +description: "A test MSIX package for Windows installation" +contents: + - src: ./dist/testapp.exe + dst: /app/testapp.exe + - src: ./testdata/acceptance/testapp/logo.png + dst: /Assets/logo.png +msix: + publisher: "CN=TestCompany, O=TestCompany, C=US" + properties: + logo: Assets/logo.png + applications: + - id: TestApp + executable: app/testapp.exe + entry_point: Windows.FullTrustApplication + visual_elements: + display_name: "Test App" + description: "A test application" + background_color: transparent + dependencies: + target_device_families: + - name: Windows.Desktop + min_version: "10.0.17763.0" + max_version_tested: "10.0.22621.0" diff --git a/testdata/acceptance/sign-msix.ps1 b/testdata/acceptance/sign-msix.ps1 new file mode 100644 index 0000000..c56c8f5 --- /dev/null +++ b/testdata/acceptance/sign-msix.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' + +# Find signtool.exe from Windows SDK +$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe | + Where-Object { $_.FullName -match 'x64' } | + Sort-Object FullName -Descending | + Select-Object -First 1 + +if (-not $signtool) { + Write-Error "signtool.exe not found" + exit 1 +} + +Write-Host "Using signtool: $($signtool.FullName)" + +& $signtool.FullName sign /fd SHA256 /a /f ./dist/test.pfx /p test123 ./dist/foo.msix + +if ($LASTEXITCODE -ne 0) { + Write-Error "signtool sign failed with exit code $LASTEXITCODE" + exit 1 +} + +Write-Host "MSIX package signed successfully" diff --git a/testdata/acceptance/testapp/logo.png b/testdata/acceptance/testapp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7639258e8e653919bbf50aabac0490fd5c85f58d GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~A&YmugAr*7po-^cSFyLX{;2{4+ z@Wp(YX*>r`o?i6N!5aj!o$D7Y1%a&#?Jrycfm;{kGcrLS_XWS6{O4VdZ0BU1kOvya N;OXk;vd$@?2>>BKEfW9$ literal 0 HcmV?d00001 diff --git a/testdata/acceptance/testapp/main.go b/testdata/acceptance/testapp/main.go new file mode 100644 index 0000000..099cd88 --- /dev/null +++ b/testdata/acceptance/testapp/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("nfpm-msix-test-ok") +} diff --git a/www/content/_index.md b/www/content/_index.md index 66dc655..7caee92 100644 --- a/www/content/_index.md +++ b/www/content/_index.md @@ -13,7 +13,7 @@ layout: hextra-home
{{< hextra/hero-subtitle >}} - A simple simple deb, rpm, apk, ipk, and arch linux packager written in Go. + A simple deb, rpm, apk, ipk, arch linux, and msix packager written in Go. {{< /hextra/hero-subtitle >}}
@@ -30,7 +30,7 @@ layout: hextra-home >}} {{< hextra/feature-card title="Multiple Formats" - subtitle="Create deb, rpm, apk, ipk, and arch linux packages." + subtitle="Create deb, rpm, apk, ipk, arch linux, and msix packages." icon="collection" >}} {{< hextra/feature-card diff --git a/www/content/docs/_index.md b/www/content/docs/_index.md index cf67169..1d9f8ee 100644 --- a/www/content/docs/_index.md +++ b/www/content/docs/_index.md @@ -15,7 +15,7 @@ This is a subtle way of saying it won't have all features, nor all formats that ## Features - **Zero Dependencies**: No Ruby, no tar, no external dependencies -- **Multiple Formats**: deb, rpm, apk, ipk, and arch linux packages +- **Multiple Formats**: deb, rpm, apk, ipk, arch linux, and msix packages - **Simple Configuration**: Single YAML file for all package formats - **Cross Platform**: Build on any platform Go supports - **Fast**: Written in Go for speed and efficiency diff --git a/www/content/docs/arch-mapping.md b/www/content/docs/arch-mapping.md index 7edac72..69e9da6 100644 --- a/www/content/docs/arch-mapping.md +++ b/www/content/docs/arch-mapping.md @@ -20,7 +20,7 @@ Thank you! --- -{{< tabs items="Deb,RPM,APK,Arch Linux,IPK" >}} +{{< tabs items="Deb,RPM,APK,Arch Linux,IPK,MSIX" >}} {{< tab >}} @@ -114,4 +114,21 @@ Thank you! {{< /tab >}} +{{< tab >}} + +| Input | Value | +| :--------: | :-------: | +| `amd64` | `x64` | +| `x86_64` | `x64` | +| `386` | `x86` | +| `i386` | `x86` | +| `i686` | `x86` | +| `arm64` | `arm64` | +| `aarch64` | `arm64` | +| `arm` | `arm` | +| `arm7` | `arm` | +| `all` | `neutral` | + +{{< /tab >}} + {{< /tabs >}} diff --git a/www/content/docs/cmd/nfpm.md b/www/content/docs/cmd/nfpm.md index 74c5f69..573d6e2 100644 --- a/www/content/docs/cmd/nfpm.md +++ b/www/content/docs/cmd/nfpm.md @@ -2,11 +2,11 @@ title: nfpm --- -Packages apps on RPM, Deb, APK, Arch Linux, and ipk formats based on a YAML configuration file +Packages apps on RPM, Deb, APK, Arch Linux, ipk, and MSIX formats based on a YAML configuration file ## Synopsis -nFPM is a simple and 0-dependencies apk, arch, deb, ipk and rpm linux packager written in Go. +nFPM is a simple and 0-dependencies apk, arch, deb, ipk, msix, and rpm packager written in Go. ## Options diff --git a/www/content/docs/cmd/nfpm_package.md b/www/content/docs/cmd/nfpm_package.md index 2688bb2..5f52f69 100644 --- a/www/content/docs/cmd/nfpm_package.md +++ b/www/content/docs/cmd/nfpm_package.md @@ -13,11 +13,11 @@ nfpm package [flags] ``` -f, --config string config file to be used (default "nfpm.yaml") -h, --help help for package - -p, --packager string which packager implementation to use [apk|archlinux|deb|ipk|rpm|srpm] + -p, --packager string which packager implementation to use [apk|archlinux|deb|ipk|msix|rpm|srpm] -t, --target string where to save the generated package (filename, folder or empty for current folder) ``` ## See also -* [nfpm](/docs/cmd/nfpm/) - Packages apps on RPM, Deb, APK, Arch Linux, and ipk formats based on a YAML configuration file +* [nfpm](/docs/cmd/nfpm/) - Packages apps on RPM, Deb, APK, Arch Linux, ipk, and MSIX formats based on a YAML configuration file diff --git a/www/content/docs/configuration.md b/www/content/docs/configuration.md index 01121f2..ad1c55d 100644 --- a/www/content/docs/configuration.md +++ b/www/content/docs/configuration.md @@ -543,6 +543,73 @@ ipk: - link_name: /usr/bin/editor target: /usr/bin/vim priority: 50 + +# Custom configuration applied only to the MSIX packager (Windows). +msix: + # msix specific architecture name that overrides "arch" without performing + # any replacements. + arch: x64 + + # Publisher identity. (required) + # Must match the subject of the signing certificate if signing is used. + publisher: "CN=MyCompany, O=MyCompany, C=US" + + # Package identity settings. + identity: + # Optional resource identifier. + resource_id: "" + + # Package display properties. + properties: + # Display name shown to users (defaults to package name). + display_name: "My Application" + # Publisher display name (defaults to package name). + publisher_display_name: "My Company" + # Path to a logo file in the package. + logo: "Assets/logo.png" + + # Applications in the package. At least one is required. + applications: + - id: App + # Path to the executable in the package. + executable: app/myapp.exe + # Entry point (defaults to Windows.FullTrustApplication). + entry_point: Windows.FullTrustApplication + # Visual presentation settings. + visual_elements: + display_name: "My Application" + description: "My application description" + # Background color (defaults to transparent). + background_color: transparent + square150x150_logo: "Assets/Square150x150Logo.png" + square44x44_logo: "Assets/Square44x44Logo.png" + + # Target device family dependencies. + # Defaults to Windows.Desktop with min version 10.0.17763.0. + dependencies: + target_device_families: + - name: Windows.Desktop + min_version: "10.0.17763.0" + max_version_tested: "10.0.22621.0" + + # Package capabilities. + capabilities: + # Standard capabilities. + capabilities: + - internetClient + # Device capabilities. + device_capabilities: + - microphone + # Restricted capabilities (require special approval). + restricted: + - broadFileSystemAccess + + # MSIX signing configuration. + # Uses PFX certificates (not PGP like Linux packagers). + signature: + # Path to the PFX certificate file. + pfx_file: certificate.pfx + # Passphrase is read from the NFPM_MSIX_PASSPHRASE environment variable. ``` ## Templating diff --git a/www/content/docs/quick-start.md b/www/content/docs/quick-start.md index b43fa9d..e9eaaa1 100644 --- a/www/content/docs/quick-start.md +++ b/www/content/docs/quick-start.md @@ -49,7 +49,7 @@ nfpm pkg --packager rpm --target /tmp/ nfpm pkg --packager apk --target /tmp/ ``` -You can also use `ipk` and `archlinux` as packagers. +You can also use `ipk`, `archlinux`, and `msix` as packagers. {{% /steps %}}