mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
fix(build): WiX MSI version mapping and album dynamic import (#1278)
This commit is contained in:
@@ -277,6 +277,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* Restores the lightweight cpal default-output path on Windows so startup no longer blocks on full device enumeration.
|
* Restores the lightweight cpal default-output path on Windows so startup no longer blocks on full device enumeration.
|
||||||
* Audio output devices on Windows and macOS now use stable backend IDs (`Wasapi:…`) with clearer labels; duplicate friendly names are disambiguated, device-change detection works again, and legacy pinned device / per-device EQ keys stored as plain names are matched after upgrade.
|
* Audio output devices on Windows and macOS now use stable backend IDs (`Wasapi:…`) with clearer labels; duplicate friendly names are disambiguated, device-change detection works again, and legacy pinned device / per-device EQ keys stored as plain names are matched after upgrade.
|
||||||
|
|
||||||
|
### Windows — MSI bundle on dev and RC versions
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1278](https://github.com/Psychotoxical/psysonic/pull/1278)**
|
||||||
|
|
||||||
|
* Windows `.msi` builds no longer fail on channel versions like `1.50.0-dev` — WiX requires a numeric fourth version field, so the bundler maps `-dev` / `-rc.N` to numeric semver in `bundle.windows.wix.version` while Settings → About still shows the real package version.
|
||||||
|
* Release builds no longer warn that the album feature barrel defeats a lazy import in the new-albums easter egg (direct import of the export helper).
|
||||||
|
|
||||||
|
|
||||||
## [1.49.0] - 2026-06-29
|
## [1.49.0] - 2026-06-29
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ Version is authoritative in `package.json` and `package-lock.json`. Promotion wo
|
|||||||
- `next` version format: `X.Y.Z-rc.N`
|
- `next` version format: `X.Y.Z-rc.N`
|
||||||
- `release` version format: `X.Y.Z`
|
- `release` version format: `X.Y.Z`
|
||||||
|
|
||||||
|
WiX/MSI bundle version: alphabetic pre-releases (`-dev`, `-rc.N`) map to monotonic `major.minor.patch.build` in `bundle.windows.wix.version` via `scripts/sync-wix-bundle-version.mjs` — dev `.1`, rc `.10000+N`, stable `.65534` (in-place MSI upgrade across promotion). About/updater still show the real package version. NSIS accepts full semver without this mapping.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
1. Never edit versions manually in random commits.
|
1. Never edit versions manually in random commits.
|
||||||
|
|||||||
+2
-2
@@ -10,14 +10,14 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"tauri:dev": "npm run prebuild:release-notes && tauri dev --config src-tauri/tauri.dev.conf.json",
|
"tauri:dev": "npm run prebuild:release-notes && tauri dev --config src-tauri/tauri.dev.conf.json",
|
||||||
"tauri:build": "npm run prebuild:release-notes && tauri build",
|
"tauri:build": "npm run prebuild:release-notes && node scripts/sync-wix-bundle-version.mjs && tauri build",
|
||||||
"lint": "eslint -c eslint.config.mjs src",
|
"lint": "eslint -c eslint.config.mjs src",
|
||||||
"lint:gradual": "eslint -c eslint.config.gradual.mjs src",
|
"lint:gradual": "eslint -c eslint.config.gradual.mjs src",
|
||||||
"dep:check": "depcruise src --config .dependency-cruiser.cjs --ignore-known",
|
"dep:check": "depcruise src --config .dependency-cruiser.cjs --ignore-known",
|
||||||
"dep:check:barrels": "node scripts/check-feature-barrel-ui.mjs",
|
"dep:check:barrels": "node scripts/check-feature-barrel-ui.mjs",
|
||||||
"check:boot-chunks": "node scripts/check-boot-chunk-lucide.mjs",
|
"check:boot-chunks": "node scripts/check-boot-chunk-lucide.mjs",
|
||||||
"build:verify": "npm run build && npm run check:boot-chunks",
|
"build:verify": "npm run build && npm run check:boot-chunks",
|
||||||
"test": "npm run prebuild:release-notes && vitest run && node --test scripts/extract-release-section.test.mjs && npm run check:css-imports && npm run dep:check:barrels",
|
"test": "npm run prebuild:release-notes && vitest run && node --test scripts/extract-release-section.test.mjs scripts/wix-bundle-version.test.mjs scripts/sync-version-pipeline.test.mjs && npm run check:css-imports && npm run dep:check:barrels",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "npm run prebuild:release-notes && vitest run --coverage && npm run check:css-imports"
|
"test:coverage": "npm run prebuild:release-notes && vitest run --coverage && npm run check:css-imports"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,3 +47,8 @@ if (updatedLock !== lock) {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`Cargo.lock workspace crates already at ${version}`);
|
console.log(`Cargo.lock workspace crates already at ${version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require('child_process').execSync('node scripts/sync-wix-bundle-version.mjs', {
|
||||||
|
cwd: root,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for promotion/version sync: npm version → sync-tauri → sync-wix.
|
||||||
|
* Simulates tauri.conf.json state after the full pipeline (no file I/O).
|
||||||
|
*/
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import {
|
||||||
|
wixMappedBuildNumber,
|
||||||
|
wixVersionOverrideForPackageVersion,
|
||||||
|
} from './wix-bundle-version.mjs';
|
||||||
|
|
||||||
|
/** Mirrors sync-tauri + sync-wix effects on tauri.conf.json. */
|
||||||
|
function confAfterVersionSync(packageVersion, priorConf) {
|
||||||
|
const conf = structuredClone(priorConf);
|
||||||
|
conf.version = packageVersion;
|
||||||
|
conf.bundle ??= {};
|
||||||
|
conf.bundle.windows ??= {};
|
||||||
|
const override = wixVersionOverrideForPackageVersion(packageVersion);
|
||||||
|
conf.bundle.windows.wix = { ...(conf.bundle.windows.wix ?? {}), version: override };
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseConf = {
|
||||||
|
version: '1.49.0-dev',
|
||||||
|
bundle: {
|
||||||
|
windows: {
|
||||||
|
nsis: { installMode: 'currentUser' },
|
||||||
|
wix: { version: '1.49.0.1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('version promotion pipeline (sync-tauri + sync-wix)', () => {
|
||||||
|
it('main → next: dev to first RC increases WiX build', () => {
|
||||||
|
const conf = confAfterVersionSync('1.50.0-rc.1', baseConf);
|
||||||
|
assert.equal(conf.version, '1.50.0-rc.1');
|
||||||
|
assert.equal(conf.bundle.windows.wix.version, '1.50.0.10001');
|
||||||
|
assert.ok(
|
||||||
|
wixMappedBuildNumber('1.50.0-rc.1') > wixMappedBuildNumber('1.50.0-dev'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('next RC bump: rc.1 to rc.2', () => {
|
||||||
|
const from = confAfterVersionSync('1.50.0-rc.1', baseConf);
|
||||||
|
const conf = confAfterVersionSync('1.50.0-rc.2', from);
|
||||||
|
assert.equal(conf.version, '1.50.0-rc.2');
|
||||||
|
assert.equal(conf.bundle.windows.wix.version, '1.50.0.10002');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('next → release: RC to stable increases WiX build', () => {
|
||||||
|
const from = confAfterVersionSync('1.50.0-rc.3', baseConf);
|
||||||
|
const conf = confAfterVersionSync('1.50.0', from);
|
||||||
|
assert.equal(conf.version, '1.50.0');
|
||||||
|
assert.equal(conf.bundle.windows.wix.version, '1.50.0.65534');
|
||||||
|
assert.ok(wixMappedBuildNumber('1.50.0') > wixMappedBuildNumber('1.50.0-rc.3'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('post-release: next minor dev resets build band on new line', () => {
|
||||||
|
const from = confAfterVersionSync('1.50.0', baseConf);
|
||||||
|
const conf = confAfterVersionSync('1.51.0-dev', from);
|
||||||
|
assert.equal(conf.version, '1.51.0-dev');
|
||||||
|
assert.equal(conf.bundle.windows.wix.version, '1.51.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stable release sets highest build in line', () => {
|
||||||
|
const conf = confAfterVersionSync('2.0.0', {
|
||||||
|
version: '2.0.0-rc.1',
|
||||||
|
bundle: { windows: { wix: { version: '2.0.0.10001' } } },
|
||||||
|
});
|
||||||
|
assert.equal(conf.version, '2.0.0');
|
||||||
|
assert.equal(conf.bundle.windows.wix.version, '2.0.0.65534');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sync-tauri-version-from-package.js invokes sync-wix', () => {
|
||||||
|
it('calls sync-wix-bundle-version.mjs after updating conf.version', () => {
|
||||||
|
const source = readFileSync(new URL('./sync-tauri-version-from-package.js', import.meta.url), 'utf8');
|
||||||
|
assert.match(source, /sync-wix-bundle-version\.mjs/);
|
||||||
|
const wixCall = source.indexOf('sync-wix-bundle-version');
|
||||||
|
const versionWrite = source.indexOf('conf.version = version');
|
||||||
|
assert.ok(wixCall > versionWrite, 'sync-wix must run after conf.version is set');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Write bundle.windows.wix.version in tauri.conf.json from package.json.
|
||||||
|
* Keeps the top-level app version unchanged (About, updater metadata, filenames).
|
||||||
|
*/
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { wixVersionOverrideForPackageVersion } from './wix-bundle-version.mjs';
|
||||||
|
|
||||||
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
||||||
|
if (!version || typeof version !== 'string') {
|
||||||
|
console.error('package.json version missing');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const confPath = path.join(root, 'src-tauri', 'tauri.conf.json');
|
||||||
|
const conf = JSON.parse(fs.readFileSync(confPath, 'utf8'));
|
||||||
|
conf.bundle ??= {};
|
||||||
|
conf.bundle.windows ??= {};
|
||||||
|
conf.bundle.windows.wix ??= {};
|
||||||
|
|
||||||
|
const wixVersion = wixVersionOverrideForPackageVersion(version);
|
||||||
|
conf.bundle.windows.wix.version = wixVersion;
|
||||||
|
console.log(`tauri.conf wix.version -> ${wixVersion} (package ${version})`);
|
||||||
|
|
||||||
|
fs.writeFileSync(confPath, `${JSON.stringify(conf, null, 2)}\n`);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Map package.json semver to a monotonic WiX/MSI ProductVersion for Tauri.
|
||||||
|
*
|
||||||
|
* `bundle.windows.wix.version` must be `major.minor.patch.build` (four integers
|
||||||
|
* ≤ 65535). Alphabetic pre-releases cannot be used directly. NSIS accepts full
|
||||||
|
* semver without this mapping.
|
||||||
|
*
|
||||||
|
* Build bands (monotonic within X.Y.Z so in-place MSI upgrades work across
|
||||||
|
* dev → rc → stable):
|
||||||
|
* dev → .1
|
||||||
|
* rc.N → .10000 + N
|
||||||
|
* stable → .65534
|
||||||
|
*
|
||||||
|
* Display / About still use the real package.json version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {const} */
|
||||||
|
export const WIX_BUILD = {
|
||||||
|
DEV: 1,
|
||||||
|
RC_BASE: 10_000,
|
||||||
|
STABLE: 65_534,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_WIX_FIELD = 65_535;
|
||||||
|
|
||||||
|
/** @param {number} build */
|
||||||
|
function assertBuildField(build, label) {
|
||||||
|
if (!Number.isInteger(build) || build < 0 || build > MAX_WIX_FIELD) {
|
||||||
|
throw new Error(`WiX build field out of range for ${label}: ${build}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WiX dot version for bundle.windows.wix.version (always four parts for channels
|
||||||
|
* we ship).
|
||||||
|
* @param {string} version
|
||||||
|
*/
|
||||||
|
export function wixVersionOverrideForPackageVersion(version) {
|
||||||
|
const trimmed = version.trim();
|
||||||
|
const match = trimmed.match(/^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(\d+))?$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid semver for WiX mapping: ${trimmed}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const major = match[1];
|
||||||
|
const minor = match[2];
|
||||||
|
const patch = match[3];
|
||||||
|
const pre = match[4];
|
||||||
|
const buildPart = match[5];
|
||||||
|
const base = `${major}.${minor}.${patch}`;
|
||||||
|
|
||||||
|
if (buildPart !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Version "${trimmed}" has +build metadata — map manually or drop build for WiX`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pre === undefined) {
|
||||||
|
return `${base}.${WIX_BUILD.STABLE}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pre === 'dev') {
|
||||||
|
return `${base}.${WIX_BUILD.DEV}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rc = pre.match(/^rc\.(\d+)$/);
|
||||||
|
if (rc) {
|
||||||
|
const n = Number(rc[1]);
|
||||||
|
if (!Number.isInteger(n) || n < 1) {
|
||||||
|
throw new Error(`WiX rc index must be ≥ 1 (got rc.${rc[1]})`);
|
||||||
|
}
|
||||||
|
const build = WIX_BUILD.RC_BASE + n;
|
||||||
|
assertBuildField(build, `rc.${n}`);
|
||||||
|
return `${base}.${build}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+$/.test(pre)) {
|
||||||
|
const n = Number(pre);
|
||||||
|
const build = WIX_BUILD.RC_BASE + n;
|
||||||
|
assertBuildField(build, `pre ${pre}`);
|
||||||
|
return `${base}.${build}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Version "${trimmed}" has non-numeric pre-release "${pre}" — MSI/WiX cannot bundle it. ` +
|
||||||
|
'Use NSIS (`--bundles nsis`) or extend wix-bundle-version.mjs.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Numeric build field from mapped WiX version (for monotonicity tests). */
|
||||||
|
export function wixMappedBuildNumber(packageVersion) {
|
||||||
|
const wix = wixVersionOverrideForPackageVersion(packageVersion);
|
||||||
|
const build = Number(wix.split('.')[3]);
|
||||||
|
assertBuildField(build, packageVersion);
|
||||||
|
return build;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
WIX_BUILD,
|
||||||
|
wixMappedBuildNumber,
|
||||||
|
wixVersionOverrideForPackageVersion,
|
||||||
|
} from './wix-bundle-version.mjs';
|
||||||
|
|
||||||
|
describe('wixVersionOverrideForPackageVersion', () => {
|
||||||
|
it('maps -dev to lowest build band', () => {
|
||||||
|
assert.equal(wixVersionOverrideForPackageVersion('1.50.0-dev'), '1.50.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps -rc.N to RC base + N', () => {
|
||||||
|
assert.equal(wixVersionOverrideForPackageVersion('1.50.0-rc.3'), '1.50.0.10003');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps stable to highest build band', () => {
|
||||||
|
assert.equal(wixVersionOverrideForPackageVersion('1.50.0'), '1.50.0.65534');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps numeric pre-release to RC band', () => {
|
||||||
|
assert.equal(wixVersionOverrideForPackageVersion('1.50.0-42'), '1.50.0.10042');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('monotonic promotion builds within X.Y.Z', () => {
|
||||||
|
it('dev < rc.1 < rc.2 < stable', () => {
|
||||||
|
const chain = ['1.50.0-dev', '1.50.0-rc.1', '1.50.0-rc.2', '1.50.0'];
|
||||||
|
let prev = -1;
|
||||||
|
for (const v of chain) {
|
||||||
|
const build = wixMappedBuildNumber(v);
|
||||||
|
assert.ok(build > prev, `${v} build ${build} must exceed ${prev}`);
|
||||||
|
prev = build;
|
||||||
|
}
|
||||||
|
assert.equal(prev, WIX_BUILD.STABLE);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -76,6 +76,9 @@
|
|||||||
"windows": {
|
"windows": {
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"installMode": "currentUser"
|
"installMode": "currentUser"
|
||||||
|
},
|
||||||
|
"wix": {
|
||||||
|
"version": "1.50.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -153,7 +153,7 @@ export default function MainApp() {
|
|||||||
const handleExport = async (since: number) => {
|
const handleExport = async (since: number) => {
|
||||||
setExportPickerOpen(false);
|
setExportPickerOpen(false);
|
||||||
try {
|
try {
|
||||||
const { exportNewAlbumsImage } = await import('@/features/album');
|
const { exportNewAlbumsImage } = await import('@/features/album/utils/exportNewAlbums');
|
||||||
const result = await exportNewAlbumsImage(since);
|
const result = await exportNewAlbumsImage(since);
|
||||||
if (result) {
|
if (result) {
|
||||||
const files = result.paths.length > 1 ? ` (${result.paths.length} Dateien)` : '';
|
const files = result.paths.length > 1 ? ` (${result.paths.length} Dateien)` : '';
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Per-device EQ — when System Default is selected, profiles follow the active OS default output and switch on external output changes (PR #1274)',
|
'Per-device EQ — when System Default is selected, profiles follow the active OS default output and switch on external output changes (PR #1274)',
|
||||||
'Navidrome public share links — paste/search preview modal, anonymous queue playback, idle server queue-restore guard (PR #1275)',
|
'Navidrome public share links — paste/search preview modal, anonymous queue playback, idle server queue-restore guard (PR #1275)',
|
||||||
'Windows startup hang after #1274 — boot barrel split, stable Wasapi device IDs, legacy EQ key match (PR #1277)',
|
'Windows startup hang after #1274 — boot barrel split, stable Wasapi device IDs, legacy EQ key match (PR #1277)',
|
||||||
|
'Windows MSI bundle on dev/RC versions — numeric WiX mapping; album easter-egg import chunk (PR #1278)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* Album feature — the Albums/Random/Lossless/Label browse pages + AlbumDetail
|
* Album feature — the Albums/Random/Lossless/Label browse pages + AlbumDetail
|
||||||
* (all lazy via deep `pages/*`, not re-exported), album cards/rows/header, the
|
* (all lazy via deep `pages/*`, not re-exported), album cards/rows/header, the
|
||||||
* album track list + its mobile/header/row pieces, album detail + browse hooks,
|
* album track list + its mobile/header/row pieces, album detail + browse hooks,
|
||||||
* the album session store, per-album offline status, and album export helpers.
|
* the album session store, per-album offline status, and album card export.
|
||||||
|
* Easter-egg new-albums PNG export (`utils/exportNewAlbums`) stays off the barrel
|
||||||
|
* so MainApp can lazy-load it without pulling canvas/fs into the boot graph.
|
||||||
*
|
*
|
||||||
* Stays OUT (library-core / cross-cutting, consumed by this feature, not owned):
|
* Stays OUT (library-core / cross-cutting, consumed by this feature, not owned):
|
||||||
* the `lib/library/album*` browse query engine (shared with offline + other
|
* the `lib/library/album*` browse query engine (shared with offline + other
|
||||||
@@ -32,7 +34,6 @@ export * from './utils/albumRecency';
|
|||||||
export * from './utils/albumTrackListHelpers';
|
export * from './utils/albumTrackListHelpers';
|
||||||
export * from './utils/deriveAlbumHeaderArtistRefs';
|
export * from './utils/deriveAlbumHeaderArtistRefs';
|
||||||
export * from './utils/exportAlbumCard';
|
export * from './utils/exportAlbumCard';
|
||||||
export * from './utils/exportNewAlbums';
|
|
||||||
export { default as AlbumCard } from './components/AlbumCard';
|
export { default as AlbumCard } from './components/AlbumCard';
|
||||||
export { default as AlbumHeader } from './components/AlbumHeader';
|
export { default as AlbumHeader } from './components/AlbumHeader';
|
||||||
export { default as AlbumRow } from './components/AlbumRow';
|
export { default as AlbumRow } from './components/AlbumRow';
|
||||||
|
|||||||
Reference in New Issue
Block a user