Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ jobs:
run: |
FBTOOLS=git+https://github.com/flatpak/flatpak-builder-tools
# Keep in sync with scripts/flatpakPnpmStoreVersion.mjs FLATPAK_NODE_GENERATOR_COMMIT.
pip3 install "${FBTOOLS}@ac5a296ac6111aa2319daf532f609a067b88d8a9#subdirectory=node"
# --force-reinstall: same 0.1.0 version across commits; plain pip is a no-op on
# preinstalled images and leaves the storeDir= generator.
pip3 install --force-reinstall --no-cache-dir \
"${FBTOOLS}@ac5a296ac6111aa2319daf532f609a067b88d8a9#subdirectory=node"

- name: Check Flatpak offline pnpm sources
run: pnpm run check:flatpak-offline-pnpm
Expand Down
17 changes: 16 additions & 1 deletion .github/workflows/flatpak.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,28 @@ jobs:
run: |
FBTOOLS=git+https://github.com/flatpak/flatpak-builder-tools
# ac5a296a+: YAML storeDir: for pnpm v11 (storeDir= breaks pnpm-workspace.yaml).
pip3 install "${FBTOOLS}@ac5a296ac6111aa2319daf532f609a067b88d8a9#subdirectory=node"
# --force-reinstall: image may preinstall flatpak_node_generator==0.1.0; same
# version makes plain pip install a no-op (leaves storeDir= generator).
pip3 install --force-reinstall --no-cache-dir \
"${FBTOOLS}@ac5a296ac6111aa2319daf532f609a067b88d8a9#subdirectory=node"
# pnpm 11+ uses store v11; flatpak-node-generator defaults to v10 for lockfile 9.
PNPM_MAJOR="$(node -p "require('./package.json').packageManager.match(/^pnpm@(\\d+)/)[1]")"
STORE_VERSION="v${PNPM_MAJOR}"
flatpak-node-generator pnpm pnpm-lock.yaml \
--pnpm-store-version "$STORE_VERSION" \
-o flatpak/generated-sources.json
# Fail fast if a stale generator still emitted npmrc-style storeDir=.
node --input-type=module <<'EOF'
import fs from 'node:fs';
import { generatedSourcesStoreDirYamlViolations } from './scripts/flatpakPnpmStoreVersion.mjs';
const sources = JSON.parse(fs.readFileSync('flatpak/generated-sources.json', 'utf8'));
const violations = generatedSourcesStoreDirYamlViolations(sources);
if (violations.length) {
for (const v of violations) console.error(v.message);
process.exit(1);
}
console.log('generated-sources storeDir YAML: ok');
EOF

# flatpak/flatpak-github-actions v6 appends -${arch} to the artifact name on upload;
# keep bundle arch-agnostic here to avoid org.coloradomesh.MeshClient-aarch64-aarch64.flatpak.
Expand Down
5 changes: 4 additions & 1 deletion docs/development-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,10 @@ flatpak install --user -y flathub org.electronjs.Electron2.BaseApp//24.08
**3. Generate offline pnpm sources** (re-run whenever `pnpm-lock.yaml` changes)

```bash
pip install flatpak-node-generator
# Prefer the CI pin with --force-reinstall (images may preinstall an older 0.1.0).
# See scripts/flatpakPnpmStoreVersion.mjs FLATPAK_NODE_GENERATOR_GIT.
pip install --force-reinstall --no-cache-dir \
"git+https://github.com/flatpak/flatpak-builder-tools@ac5a296ac6111aa2319daf532f609a067b88d8a9#subdirectory=node"
# Must match package.json packageManager major (pnpm 11 → v11). Generator defaults to v10.
PNPM_MAJOR="$(node -p "require('./package.json').packageManager.match(/^pnpm@(\\d+)/)[1]")"
STORE_VERSION="v${PNPM_MAJOR}"
Expand Down
6 changes: 4 additions & 2 deletions org.coloradomesh.MeshClient.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ modules:
env:
PNPM_HOME: /run/build/mesh-client/.pnpm
pnpm_config_cache: /run/build/mesh-client/.npm
# Values must be YAML strings (quoted). Unquoted true/false become JSON
# booleans and flatpak-builder drops the entire env map (GStrv warning).
# pnpm 11 supply-chain / verifyDepsBeforeRun must not hit the registry offline.
PNPM_CONFIG_TRUST_LOCKFILE: true
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false
PNPM_CONFIG_TRUST_LOCKFILE: 'true'
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false'
build-commands:
# Install pnpm standalone from archived release (no network needed).
# Archive extracts to pnpm-vendor/ (strip-components: 0) so the root `pnpm` binary is
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-flatpak-offline-pnpm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function generateOfflineSources(expectedStoreVersion) {
`flatpak-node-generator not found on PATH (and FLATPAK_NODE_GENERATOR unset).\n` +
` Install the CI pin, then re-run:\n` +
` python3 -m venv .cache/flatpak-node-venv\n` +
` .cache/flatpak-node-venv/bin/pip install '${FLATPAK_NODE_GENERATOR_GIT}'\n` +
` .cache/flatpak-node-venv/bin/pip install --force-reinstall --no-cache-dir '${FLATPAK_NODE_GENERATOR_GIT}'\n` +
` export PATH="$PWD/.cache/flatpak-node-venv/bin:$PATH"\n` +
` pnpm run check:flatpak-offline-pnpm`,
};
Expand Down
14 changes: 13 additions & 1 deletion scripts/check-flatpak.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { offlinePnpmEnvContractViolations } from './flatpakOfflinePnpmEnv.mjs';
import {
flatpakWorkflowGeneratorInstallViolations,
flatpakWorkflowStoreVersionViolations,
storeVersionFromPackageManager,
} from './flatpakPnpmStoreVersion.mjs';
Expand All @@ -15,6 +16,7 @@ const METAINFO = path.join(ROOT, 'flatpak', 'org.coloradomesh.MeshClient.metainf
const DESKTOP = path.join(ROOT, 'flatpak', 'org.coloradomesh.MeshClient.desktop');
const MANIFEST = path.join(ROOT, 'org.coloradomesh.MeshClient.yml');
const FLATPAK_WORKFLOW = path.join(ROOT, '.github/workflows/flatpak.yaml');
const CI_WORKFLOW = path.join(ROOT, '.github/workflows/ci.yaml');
const WRAPPER = path.join(ROOT, 'flatpak', 'mesh-client-wrapper.sh');
const PKG = path.join(ROOT, 'package.json');
const EXPECTED_APP_ID = 'org.coloradomesh.MeshClient';
Expand Down Expand Up @@ -189,7 +191,17 @@ function checkFlatpakWorkflowStoreVersion(pkg) {
if (!expected) return violations;

const yaml = fs.readFileSync(FLATPAK_WORKFLOW, 'utf8');
return flatpakWorkflowStoreVersionViolations(yaml, expected);
violations.push(...flatpakWorkflowStoreVersionViolations(yaml, expected));

// CI installs the same pin for check:flatpak-offline-pnpm — must also force-reinstall.
if (fs.existsSync(CI_WORKFLOW)) {
const ciYaml = fs.readFileSync(CI_WORKFLOW, 'utf8');
violations.push(
...flatpakWorkflowGeneratorInstallViolations(ciYaml, '.github/workflows/ci.yaml'),
);
}

return violations;
}

function checkManifestBranchAndElectronPayload(pkg) {
Expand Down
23 changes: 13 additions & 10 deletions scripts/flatpak-pnpm-bin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,36 @@ describe('Flatpak pnpm standalone install', () => {
expect(yaml).toMatch(/cp -a pnpm-vendor\/dist \/run\/build\/mesh-client\/\.pnpm-bin\/dist/);
});

it('requires unquoted offline pnpm booleans in mesh-client build-options.env', () => {
it('requires quoted offline pnpm env strings for flatpak-builder GStrv', () => {
const yaml = fs.readFileSync(MANIFEST, 'utf8');
const env = parseMeshClientModuleBuildEnv(yaml);
expect(env).not.toBeNull();
expect(env?.PNPM_CONFIG_TRUST_LOCKFILE).toBe(true);
expect(env?.PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN).toBe(false);
expect(env?.PNPM_CONFIG_TRUST_LOCKFILE).toBe('true');
expect(env?.PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN).toBe('false');
expect(offlinePnpmEnvContractViolations(yaml)).toEqual([]);
});

it('rejects quoted or commented offline pnpm env values', () => {
const quoted = `
it('rejects unquoted YAML booleans (GStrv drops the whole env map)', () => {
const unquoted = `
modules:
- name: mesh-client
build-options:
env:
PNPM_CONFIG_TRUST_LOCKFILE: 'true'
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false'
PNPM_CONFIG_TRUST_LOCKFILE: true
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false
`;
expect(offlinePnpmEnvContractViolations(quoted).length).toBe(2);
expect(offlinePnpmEnvContractViolations(unquoted).length).toBe(2);
expect(offlinePnpmEnvContractViolations(unquoted)[0].message).toMatch(/GStrv|quoted/);
});

it('rejects missing or commented offline pnpm env values', () => {
const commentedOnly = `
modules:
- name: mesh-client
build-options:
env:
# PNPM_CONFIG_TRUST_LOCKFILE: true
# PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false
# PNPM_CONFIG_TRUST_LOCKFILE: 'true'
# PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false'
PNPM_HOME: /run/build/mesh-client/.pnpm
`;
expect(offlinePnpmEnvContractViolations(commentedOnly).length).toBe(2);
Expand Down
46 changes: 46 additions & 0 deletions scripts/flatpak-pnpm-install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import { fileURLToPath } from 'url';
import path from 'path';
import { cleanJsrTempDirs } from './clean-jsr-temp-dirs.mjs';
import {
stripNpmrcStoreDirLines,
stripPnpmWorkspaceStoreDirLines,
} from './flatpakPnpmStoreVersion.mjs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
Expand All @@ -36,6 +40,46 @@
STORE_DIR,
];

/**
* Remove generator-appended storeDir / store-dir lines so pnpm can parse the
* workspace and `--store-dir` (sandbox path) wins over a host-cache path.
*
* @param {string} [root]
* @returns {{ workspaceRemoved: number, npmrcRemoved: number }}
*/
export function sanitizeFlatpakPnpmStoreDirConfig(root = projectRoot) {
const workspacePath = path.join(root, 'pnpm-workspace.yaml');
const npmrcPath = path.join(root, '.npmrc');
let workspaceRemoved = 0;
let npmrcRemoved = 0;

if (fs.existsSync(workspacePath)) {
const before = fs.readFileSync(workspacePath, 'utf8');
const { yaml, removed } = stripPnpmWorkspaceStoreDirLines(before);
workspaceRemoved = removed;
if (removed > 0) {
fs.writeFileSync(workspacePath, yaml, 'utf8');
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
console.warn(
`[flatpak-pnpm] stripped ${removed} storeDir line(s) from pnpm-workspace.yaml (using --store-dir ${STORE_DIR})`,
);
}
}

if (fs.existsSync(npmrcPath)) {
const before = fs.readFileSync(npmrcPath, 'utf8');
const { text, removed } = stripNpmrcStoreDirLines(before);
npmrcRemoved = removed;
if (removed > 0) {
fs.writeFileSync(npmrcPath, text, 'utf8');
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
console.warn(
`[flatpak-pnpm] stripped ${removed} store-dir line(s) from .npmrc (using --store-dir ${STORE_DIR})`,
);
}
}

return { workspaceRemoved, npmrcRemoved };
}

export function runFlatpakPnpmInstall() {
if (!fs.existsSync(STORE_DIR)) {
console.error(
Expand All @@ -44,6 +88,8 @@
process.exit(1);
}

sanitizeFlatpakPnpmStoreDirConfig(projectRoot);

let lastStatus = 1;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
Expand Down
39 changes: 36 additions & 3 deletions scripts/flatpak-pnpm-install.test.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
// @vitest-environment node
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { describe, expect, it } from 'vitest';
import { FLATPAK_PNPM_INSTALL_ARGS } from './flatpak-pnpm-install.mjs';
import { fileURLToPath } from 'url';
import { afterEach, describe, expect, it } from 'vitest';
import {
FLATPAK_PNPM_INSTALL_ARGS,
sanitizeFlatpakPnpmStoreDirConfig,
} from './flatpak-pnpm-install.mjs';

const scriptPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'flatpak-pnpm-install.mjs',
);

/** @type {string[]} */
const tempRoots = [];

afterEach(() => {
for (const root of tempRoots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});

describe('flatpak-pnpm-install.mjs', () => {
it('exits non-zero when offline store is unavailable outside sandbox', () => {
const result = spawnSync(process.execPath, [scriptPath], {
Expand All @@ -25,5 +39,24 @@ describe('flatpak-pnpm-install.mjs', () => {
expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--offline');
expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--frozen-lockfile');
expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--ignore-scripts');
expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--store-dir');
});

it('strips generator storeDir= from workspace before install (Flatpak CI regression)', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mesh-flatpak-sanitize-'));
tempRoots.push(root);
fs.writeFileSync(
path.join(root, 'pnpm-workspace.yaml'),
'nodeLinker: hoisted\nstoreDir=/__w/mesh-client/bad-store\n',
'utf8',
);
fs.writeFileSync(path.join(root, '.npmrc'), 'store-dir=/__w/host\n', 'utf8');

const result = sanitizeFlatpakPnpmStoreDirConfig(root);
expect(result).toEqual({ workspaceRemoved: 1, npmrcRemoved: 1 });
expect(fs.readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8')).toBe(
'nodeLinker: hoisted\n',
);
expect(fs.readFileSync(path.join(root, '.npmrc'), 'utf8')).toBe('');
});
});
20 changes: 13 additions & 7 deletions scripts/flatpakOfflinePnpmEnv.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
* Shared Flatpak manifest contract for offline pnpm 11 install.
*
* Failure point: loose YAML regex can match commented or quoted keys outside the
* mesh-client module env map. Fallback: parse only that scoped env block and
* require exact unquoted YAML booleans.
* mesh-client module env map. Fallback: parse only that scoped env block.
*
* Failure point: flatpak-builder deserializes `env` as GStrv (string values only).
* Unquoted YAML `true`/`false` become JSON booleans → entire env map is dropped
* (`Failed to deserialize "env" property of type "GStrv"`). Fallback: require
* quoted `'true'` / `'false'` strings.
*/

/**
Expand Down Expand Up @@ -47,7 +51,6 @@ export function parseMeshClientModuleBuildEnv(yaml) {
(raw.startsWith("'") && raw.endsWith("'")) ||
(raw.startsWith('"') && raw.endsWith('"'))
) {
// Quoted values are strings, not YAML booleans — reject for required keys.
env[key] = raw.slice(1, -1);
} else {
env[key] = raw;
Expand Down Expand Up @@ -78,19 +81,22 @@ export function offlinePnpmEnvContractViolations(
return violations;
}

if (env.PNPM_CONFIG_TRUST_LOCKFILE !== true) {
// GStrv requires string values — quoted 'true'/'false', not YAML booleans.
if (env.PNPM_CONFIG_TRUST_LOCKFILE !== 'true') {
violations.push({
file: fileRel,
message:
'manifest mesh-client build-options.env must set PNPM_CONFIG_TRUST_LOCKFILE: true (unquoted boolean; skip registry supply-chain re-verify offline)',
"manifest mesh-client build-options.env must set PNPM_CONFIG_TRUST_LOCKFILE: 'true' " +
'(quoted string for flatpak-builder GStrv; unquoted true drops the whole env map)',
});
}

if (env.PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN !== false) {
if (env.PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN !== 'false') {
violations.push({
file: fileRel,
message:
'manifest mesh-client build-options.env must set PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false (unquoted boolean; pnpm run must not auto-install offline)',
"manifest mesh-client build-options.env must set PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false' " +
'(quoted string for flatpak-builder GStrv; unquoted false drops the whole env map)',
});
}

Expand Down
Loading