fix: ensure ommitting the productcode will generate one

This commit is contained in:
Dj Gilcrease
2026-06-13 05:07:45 -07:00
parent 1d7cb04ec5
commit f04205a7eb
5 changed files with 57 additions and 10 deletions
+28 -4
View File
@@ -3,6 +3,7 @@
package msi
import (
"crypto/sha1"
"errors"
"fmt"
"hash/fnv"
@@ -125,12 +126,22 @@ func (m *MSI) Package(info *nfpm.Info, w io.Writer) error {
WithVersion(convertToMSIVersion(info.Version)).
WithAllUsers(*info.MSI.AllUsers)
if info.MSI.ProductCode != "" {
b = b.WithProductCode(info.MSI.ProductCode)
// ProductCode must always be present. When omitted we derive a stable GUID
// from the product name (kept constant across versions so it does not change
// on every version bump).
productCode := info.MSI.ProductCode
if productCode == "" {
productCode = deriveGUID("product|" + info.MSI.ProductName)
}
if info.MSI.UpgradeCode != "" {
b = b.WithUpgradeCode(info.MSI.UpgradeCode)
b = b.WithProductCode(productCode)
// UpgradeCode stays stable across versions; derive it from the product name
// alone when omitted so upgrades work out of the box.
upgradeCode := info.MSI.UpgradeCode
if upgradeCode == "" {
upgradeCode = deriveGUID("upgrade|" + info.MSI.ProductName)
}
b = b.WithUpgradeCode(upgradeCode)
for k, v := range info.MSI.Properties {
b = b.WithProperty(k, v)
}
@@ -558,6 +569,19 @@ func looksLikeGUID(s string) bool {
return strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}")
}
// deriveGUID produces a stable, braced uppercase GUID (RFC 4122 v5 style) from
// the given seed. The same seed always yields the same GUID, keeping builds
// reproducible.
func deriveGUID(seed string) string {
h := sha1.Sum([]byte("nfpm-msi:" + seed))
var b [16]byte
copy(b[:], h[:16])
b[6] = (b[6] & 0x0f) | 0x50 // version 5
b[8] = (b[8] & 0x3f) | 0x80 // RFC 4122 variant
s := fmt.Sprintf("%X", b[:])
return fmt.Sprintf("{%s-%s-%s-%s-%s}", s[0:8], s[8:12], s[12:16], s[16:20], s[20:32])
}
// convertToMSIVersion converts a semver-style version to MSI's
// Major.Minor.Build format. Each field is numeric and clamped to 65535.
func convertToMSIVersion(version string) string {
+21
View File
@@ -9,6 +9,7 @@ import (
"math/big"
"os"
"path/filepath"
"regexp"
"testing"
"time"
@@ -168,6 +169,26 @@ func TestExplicitGUIDs(t *testing.T) {
packageAndValidate(t, info)
}
// TestDerivedProductCode guards against shipping an MSI without a ProductCode
// (msiexec fails such installs with error 1605). When the config omits the
// codes, a derived braced GUID must be written into the package.
func TestDerivedProductCode(t *testing.T) {
info := exampleInfo()
require.Empty(t, info.MSI.ProductCode)
require.Empty(t, info.MSI.UpgradeCode)
var buf bytes.Buffer
require.NoError(t, msi.Default.Package(info, &buf))
guid := regexp.MustCompile(`\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}`)
require.True(t, guid.Match(buf.Bytes()), "a derived GUID must be present in the package")
// Derivation must be reproducible.
var buf2 bytes.Buffer
require.NoError(t, msi.Default.Package(exampleInfo(), &buf2))
require.Equal(t, buf.Bytes(), buf2.Bytes(), "builds with identical input must be reproducible")
}
func TestShortcut(t *testing.T) {
info := exampleInfo()
info.MSI.Shortcuts = []nfpm.MSIShortcut{
+2 -2
View File
@@ -590,8 +590,8 @@ type MSI struct {
Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in msi nomenclature"`
ProductName string `yaml:"product_name,omitempty" json:"product_name,omitempty" jsonschema:"title=product name,description=defaults to the package name"`
Manufacturer string `yaml:"manufacturer" json:"manufacturer" jsonschema:"title=manufacturer/author of the product,example=My Company"`
ProductCode string `yaml:"product_code,omitempty" json:"product_code,omitempty" jsonschema:"title=product code GUID,description=auto-derived when empty,example={12345678-1234-1234-1234-123456789ABC}"`
UpgradeCode string `yaml:"upgrade_code,omitempty" json:"upgrade_code,omitempty" jsonschema:"title=upgrade code GUID,description=recommended for upgrades,example={ABCDEF01-2345-6789-ABCD-EF0123456789}"`
ProductCode string `yaml:"product_code,omitempty" json:"product_code,omitempty" jsonschema:"title=product code GUID,description=derived from the product name (stable across versions) when empty,example={12345678-1234-1234-1234-123456789ABC}"`
UpgradeCode string `yaml:"upgrade_code,omitempty" json:"upgrade_code,omitempty" jsonschema:"title=upgrade code GUID,description=derived from the product name when empty,example={ABCDEF01-2345-6789-ABCD-EF0123456789}"`
InstallDir string `yaml:"install_dir,omitempty" json:"install_dir,omitempty" jsonschema:"title=default install folder name,description=defaults to the product name"`
AllUsers *bool `yaml:"all_users,omitempty" json:"all_users,omitempty" jsonschema:"title=per-machine install,description=defaults to true"`
Properties map[string]string `yaml:"properties,omitempty" json:"properties,omitempty" jsonschema:"title=arbitrary MSI Property rows"`
+4 -2
View File
@@ -634,10 +634,12 @@ msi:
# Manufacturer/author of the product. (required)
manufacturer: "My Company"
# Product code GUID. Auto-derived (stable) when omitted.
# Product code GUID. When omitted, a stable GUID is derived from the product
# name (kept constant across versions).
product_code: "{12345678-1234-1234-1234-123456789ABC}"
# Upgrade code GUID. Recommended so future versions upgrade in place.
# Upgrade code GUID. When omitted, a stable GUID is derived from the product
# name (kept constant across versions so upgrades work).
upgrade_code: "{ABCDEF01-2345-6789-ABCD-EF0123456789}"
# Name of the default install folder (defaults to product_name).
+2 -2
View File
@@ -677,7 +677,7 @@
"product_code": {
"type": "string",
"title": "product code GUID",
"description": "auto-derived when empty",
"description": "derived from the product name (stable across versions) when empty",
"examples": [
"{12345678-1234-1234-1234-123456789ABC}"
]
@@ -685,7 +685,7 @@
"upgrade_code": {
"type": "string",
"title": "upgrade code GUID",
"description": "recommended for upgrades",
"description": "derived from the product name when empty",
"examples": [
"{ABCDEF01-2345-6789-ABCD-EF0123456789}"
]