diff --git a/org.coloradomesh.MeshClient.yml b/org.coloradomesh.MeshClient.yml index 60853d061..62dbe32a7 100644 --- a/org.coloradomesh.MeshClient.yml +++ b/org.coloradomesh.MeshClient.yml @@ -40,7 +40,8 @@ modules: pnpm_config_cache: /run/build/mesh-client/.npm build-commands: # Install pnpm standalone binary from archived release (no network needed). - # Archive extracts to pnpm-vendor/ so the bundled dist/ does not collide with app dist/. + # Archive extracts to pnpm-vendor/ (strip-components: 0) so the root `pnpm` binary is + # kept and the bundled dist/ does not collide with app dist/. - install -Dm755 pnpm-vendor/pnpm /run/build/mesh-client/.pnpm-bin/pnpm # Offline install using generated-sources.json (retries @jsr temp-dir races). # --store-dir must use the sandbox-absolute path because the type:shell @@ -81,11 +82,14 @@ modules: url: https://github.com/pnpm/pnpm/releases/download/v11.15.1/pnpm-linux-x64.tar.gz sha256: 0c1373b6390f6b89ff8b896d3647c9826577ddc49173a2f732da69b36569f9e0 dest: pnpm-vendor + # pnpm tarball has root-level `pnpm` + `dist/`; default strip-components:1 drops the binary. + strip-components: 0 only-arches: [x86_64] - type: archive url: https://github.com/pnpm/pnpm/releases/download/v11.15.1/pnpm-linux-arm64.tar.gz sha256: 361e385867146972d0635a41a1871cb44c9c23f65acce78a5f1ca1d44ac0afcd dest: pnpm-vendor + strip-components: 0 only-arches: [aarch64] - type: archive url: https://github.com/electron/electron/releases/download/v41.10.2/electron-v41.10.2-linux-x64.zip diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6c8d62f3..791ed4748 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,13 @@ overrides: '@meshtastic/core': npm:@jsr/meshtastic__core@^2.6.6 cacheable-request: ^10.0.0 form-data: ^4.0.6 - js-yaml: ^4.2.0 + brace-expansion@<1.1.16: 1.1.16 + brace-expansion@>=2.0.0 <2.1.2: 2.1.2 + brace-expansion@>=3.0.0 <5.0.7: 5.0.7 + js-yaml: ^4.3.0 markdown-it@<=14.1.1: '>=14.2.0 <15' - shell-quote: ^1.8.4 - tar: ^7.5.16 + shell-quote: ^1.9.0 + tar: ^7.5.18 tmp: ^0.2.6 undici: ^7.28.0 undici-types: ^7.28.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e3d798f82..667dde940 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -37,10 +37,15 @@ overrides: '@meshtastic/core': npm:@jsr/meshtastic__core@^2.6.6 cacheable-request: ^10.0.0 form-data: ^4.0.6 - js-yaml: ^4.2.0 + # Security floors (Dependabot GHSA-52cp-r559-cp3m / GHSA-395f-4hp3-45gv / + # GHSA-w8wr-v893-vjvp / GHSA-3jxr-9vmj-r5cp). Keep majors separate for brace-expansion. + 'brace-expansion@<1.1.16': 1.1.16 + 'brace-expansion@>=2.0.0 <2.1.2': 2.1.2 + 'brace-expansion@>=3.0.0 <5.0.7': 5.0.7 + js-yaml: ^4.3.0 markdown-it@<=14.1.1: '>=14.2.0 <15' - shell-quote: ^1.8.4 - tar: ^7.5.16 + shell-quote: ^1.9.0 + tar: ^7.5.18 tmp: ^0.2.6 undici: ^7.28.0 undici-types: ^7.28.0 diff --git a/scripts/check-flatpak.mjs b/scripts/check-flatpak.mjs index 23fd9e70c..32da05235 100644 --- a/scripts/check-flatpak.mjs +++ b/scripts/check-flatpak.mjs @@ -142,6 +142,24 @@ function checkManifestPnpmVersion(pkg) { }); } + // pnpm 11+ ships tar.gz with root-level `pnpm` + `dist/`. flatpak-builder defaults to + // strip-components:1, which discards the binary (only dist/ contents remain under dest). + if (yaml.includes('pnpm-linux-') && yaml.includes('.tar.gz')) { + const pnpmArchiveBlocks = yaml + .split(/^\s*- type: archive\s*$/m) + .filter((block) => /pnpm-linux-.*\.tar\.gz/.test(block)); + for (const block of pnpmArchiveBlocks) { + if (!/strip-components:\s*0\b/.test(block)) { + violations.push({ + file: rel, + message: + 'pnpm linux tar.gz archive sources must set strip-components: 0 (default 1 drops root-level pnpm binary)', + }); + break; + } + } + } + return violations; } diff --git a/scripts/check-package-manager.mjs b/scripts/check-package-manager.mjs index 899981c46..97f6f98a8 100644 --- a/scripts/check-package-manager.mjs +++ b/scripts/check-package-manager.mjs @@ -49,9 +49,26 @@ export function parsePackageManagerField(packageManager) { return { name: 'pnpm', version: raw, ...parsed }; } +/** + * Prefer lifecycle user-agent (set while pnpm runs preinstall/dev) over PATH lookup. + * Windows `spawnSync('pnpm')` without `shell: true` cannot resolve `.cmd` shims. + * @param {string | undefined} userAgent + * @returns {string | null} + */ +export function pnpmVersionFromUserAgent(userAgent) { + if (typeof userAgent !== 'string' || !userAgent) return null; + const match = userAgent.match(/(?:^|\s)pnpm\/(\d+\.\d+\.\d+)/); + return match?.[1] ?? null; +} + /** @returns {boolean} */ export function hasCorepack() { - const res = spawnSync('corepack', ['--version'], { encoding: 'utf8', stdio: 'pipe' }); + const res = spawnSync('corepack', ['--version'], { + encoding: 'utf8', + stdio: 'pipe', + // Windows: corepack is a .cmd shim; spawn without shell cannot resolve it. + shell: process.platform === 'win32', + }); return res.status === 0 && !res.error; } @@ -192,7 +209,16 @@ function readPackageJson(root = repoRoot) { } function currentPnpmVersion() { - const res = spawnSync('pnpm', ['--version'], { encoding: 'utf8', stdio: 'pipe' }); + const fromUa = pnpmVersionFromUserAgent(process.env.npm_config_user_agent); + if (fromUa) return fromUa; + + const res = spawnSync('pnpm', ['--version'], { + encoding: 'utf8', + stdio: 'pipe', + // Windows: pnpm/action-setup and Corepack install .cmd shims; without shell, + // spawnSync cannot find them and preinstall reports "You have: not found". + shell: process.platform === 'win32', + }); if (res.error || res.status !== 0) return null; return String(res.stdout || '').trim() || null; } diff --git a/scripts/check-package-manager.test.mjs b/scripts/check-package-manager.test.mjs index da7a53b9d..aaf0f29c2 100644 --- a/scripts/check-package-manager.test.mjs +++ b/scripts/check-package-manager.test.mjs @@ -9,6 +9,7 @@ import { parseEngineFloor, parsePackageManagerField, parseSemver, + pnpmVersionFromUserAgent, } from './check-package-manager.mjs'; describe('check-package-manager parseSemver', () => { @@ -38,6 +39,14 @@ describe('check-package-manager parseEngineFloor', () => { }); }); +describe('check-package-manager pnpmVersionFromUserAgent', () => { + it('reads pnpm version from lifecycle user-agent', () => { + expect(pnpmVersionFromUserAgent('pnpm/11.15.1 npm/? node/v22.23.1 win32 x64')).toBe('11.15.1'); + expect(pnpmVersionFromUserAgent('npm/10.9.0 node/v22.23.1')).toBeNull(); + expect(pnpmVersionFromUserAgent(undefined)).toBeNull(); + }); +}); + describe('check-package-manager upgrade hints', () => { it('uses corepack when available', () => { expect(buildPnpmUpgradeHintLines('11.15.1', { corepackAvailable: true })[0]).toBe( diff --git a/scripts/sync-flatpak-electron.test.mjs b/scripts/sync-flatpak-electron.test.mjs index d12f962b6..1277160d0 100644 --- a/scripts/sync-flatpak-electron.test.mjs +++ b/scripts/sync-flatpak-electron.test.mjs @@ -20,6 +20,7 @@ const SAMPLE_MANIFEST = ` - type: archive url: https://github.com/pnpm/pnpm/releases/download/v11.15.1/pnpm-linux-arm64.tar.gz sha256: 361e385867146972d0635a41a1871cb44c9c23f65acce78a5f1ca1d44ac0afcd dest: pnpm-vendor + strip-components: 0 only-arches: [aarch64] - type: archive url: https://github.com/electron/electron/releases/download/v41.10.0/electron-v41.10.0-linux-x64.zip