feat: add alpha msix support (#1051)

This commit is contained in:
Dj Gilcrease
2026-03-20 07:14:55 -07:00
committed by GitHub
parent 28602db2f3
commit 432fb6710a
24 changed files with 1010 additions and 15 deletions
+40 -1
View File
@@ -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
+17
View File
@@ -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:
+55
View File
@@ -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")
})
}
}
+1 -1
View File
@@ -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 != "" {
+3
View File
@@ -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
)
+6
View File
@@ -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=
+3 -2
View File
@@ -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,
+324
View File
@@ -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, ".")
}
+244
View File
@@ -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 <test@example.com>",
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())
}
+78 -2
View File
@@ -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 == "" {
+20
View File
@@ -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'
+40
View File
@@ -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
}
+28
View File
@@ -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"
+28
View File
@@ -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"
+23
View File
@@ -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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

+7
View File
@@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("nfpm-msix-test-ok")
}
+2 -2
View File
@@ -13,7 +13,7 @@ layout: hextra-home
<div class="hx:mb-12">
{{< 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 >}}
</div>
@@ -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
+1 -1
View File
@@ -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
+18 -1
View File
@@ -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 >}}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+67
View File
@@ -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
+1 -1
View File
@@ -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 %}}