mirror of
https://github.com/goreleaser/nfpm.git
synced 2026-06-19 08:05:04 +00:00
feat: add alpha msix support (#1051)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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, ".")
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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'
|
||||
Vendored
+40
@@ -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
|
||||
}
|
||||
Vendored
+28
@@ -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
@@ -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"
|
||||
Vendored
+23
@@ -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"
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 124 B |
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("nfpm-msix-test-ok")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user