Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "electron-vite build",
"native:build": "node scripts/build-cursor-overlay-helper.cjs",
"native:build-overlay": "npm run native:build",
"package:win": "npm run build && npm run native:build && electron-builder --win --x64 --publish never",
"package:win": "npm run build && npm run native:build && electron-builder --win --x64 --publish never && node scripts/sign-win-artifacts.cjs --update-latest-yml",
"package:win:verify-uiaccess": "node scripts/verify-win-uiaccess-package.cjs",
"signing:create-dev-cert": "node scripts/create-dev-signing-cert.cjs",
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json",
Expand Down
79 changes: 79 additions & 0 deletions scripts/sign-win-artifacts.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const {
findWindowsSdkTool,
isWindows,
Expand Down Expand Up @@ -37,7 +38,85 @@ module.exports = async function signWindowsArtifacts(buildResult) {
continue;
}
runTool(signtoolExe, signingArgs);
updateLatestYmlForSignedInstaller(artifactPath);
}

return artifacts;
};

if (require.main === module) {
const command = process.argv[2];
if (command !== '--update-latest-yml') {
throw new Error('Unsupported command. Use --update-latest-yml.');
}

updateLatestYmlForReferencedInstaller(resolveProjectPath('dist', 'latest.yml'));
}

function updateLatestYmlForSignedInstaller(installerPath) {
const latestPath = path.join(path.dirname(installerPath), 'latest.yml');
if (!fs.existsSync(latestPath)) {
return;
}

const installerName = path.basename(installerPath);
const sha512 = createSha512Base64(installerPath);
const content = fs.readFileSync(latestPath, 'utf8');

if (!referencesInstaller(content, installerName)) {
console.warn(`WARNING: Skipping latest.yml update because it does not reference ${installerName}.`);
return;
}

const updated = content.replace(/^(\s*sha512:\s*).+$/gm, `$1${sha512}`);

if (updated === content) {
throw new Error('latest.yml did not contain any sha512 entries to update.');
}

fs.writeFileSync(latestPath, updated);
console.log(`Updated latest.yml sha512 for ${installerName}.`);
}

function updateLatestYmlForReferencedInstaller(latestPath) {
if (!fs.existsSync(latestPath)) {
return;
}

const content = fs.readFileSync(latestPath, 'utf8');
const installerName = findInstallerNameInLatestYml(content);
const installerPath = path.join(path.dirname(latestPath), installerName);

if (!fs.existsSync(installerPath)) {
throw new Error(`latest.yml references missing installer ${installerName}.`);
}

updateLatestYmlForSignedInstaller(installerPath);
}

function createSha512Base64(filePath) {
return crypto.createHash('sha512').update(fs.readFileSync(filePath)).digest('base64');
}

function findInstallerNameInLatestYml(content) {
const match = content.match(/^\s*(?:url|path):\s*['"]?([^'"\r\n]+\.exe)['"]?\s*$/im);
if (!match) {
throw new Error('latest.yml does not reference a Windows installer.');
}

return path.basename(decodeURI(match[1]));
}

function referencesInstaller(content, installerName) {
const encodedInstallerName = encodeURI(installerName);
return (
content.includes(`url: ${installerName}`) ||
content.includes(`path: ${installerName}`) ||
content.includes(`url: ${encodedInstallerName}`) ||
content.includes(`path: ${encodedInstallerName}`)
);
}

module.exports.updateLatestYmlForSignedInstaller = updateLatestYmlForSignedInstaller;
module.exports.updateLatestYmlForReferencedInstaller = updateLatestYmlForReferencedInstaller;
module.exports.createSha512Base64 = createSha512Base64;
136 changes: 136 additions & 0 deletions src/main/sign-win-artifacts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { afterEach, describe, expect, it } from 'vitest';

const require = createRequire(import.meta.url);
const {
createSha512Base64,
updateLatestYmlForReferencedInstaller,
updateLatestYmlForSignedInstaller
} = require('../../scripts/sign-win-artifacts.cjs') as {
createSha512Base64(filePath: string): string;
updateLatestYmlForReferencedInstaller(latestPath: string): void;
updateLatestYmlForSignedInstaller(installerPath: string): void;
};

const tempDirs: string[] = [];

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

describe('sign-win-artifacts updater metadata', () => {
it('updates latest.yml checksums after installer bytes change', () => {
const distDir = createTempDir();
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
const latestPath = path.join(distDir, 'latest.yml');
fs.writeFileSync(installerPath, 'unsigned installer bytes');
fs.writeFileSync(
latestPath,
[
'version: 0.1.1',
'files:',
' - url: Switchify-PC-Setup-0.1.1-x64.exe',
' sha512: old-file-checksum',
'path: Switchify-PC-Setup-0.1.1-x64.exe',
'sha512: old-path-checksum'
].join('\n')
);

fs.writeFileSync(installerPath, 'signed installer bytes');
updateLatestYmlForSignedInstaller(installerPath);

const sha512 = createSha512Base64(installerPath);
const latest = fs.readFileSync(latestPath, 'utf8');
expect(latest.match(/^sha512: .+$/gm)).toEqual([`sha512: ${sha512}`]);
expect(latest.match(/^\s+sha512: .+$/gm)).toEqual([` sha512: ${sha512}`]);
});

it('ignores latest.yml when it does not reference the signed installer', () => {
const distDir = createTempDir();
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
fs.writeFileSync(installerPath, 'signed installer bytes');
fs.writeFileSync(
path.join(distDir, 'latest.yml'),
[
'version: 0.1.1',
'files:',
' - url: Other-Setup.exe',
' sha512: old-file-checksum',
'path: Other-Setup.exe',
'sha512: old-path-checksum'
].join('\n')
);

expect(() => updateLatestYmlForSignedInstaller(installerPath)).not.toThrow();
expect(fs.readFileSync(path.join(distDir, 'latest.yml'), 'utf8')).toContain('old-file-checksum');
});

it('updates latest.yml when the installer name is URL-encoded', () => {
const distDir = createTempDir();
const installerPath = path.join(distDir, 'Switchify PC Setup 0.1.1 x64.exe');
const latestPath = path.join(distDir, 'latest.yml');
fs.writeFileSync(installerPath, 'signed installer bytes');
fs.writeFileSync(
latestPath,
[
'version: 0.1.1',
'files:',
' - url: Switchify%20PC%20Setup%200.1.1%20x64.exe',
' sha512: old-file-checksum',
'path: Switchify%20PC%20Setup%200.1.1%20x64.exe',
'sha512: old-path-checksum'
].join('\n')
);

updateLatestYmlForSignedInstaller(installerPath);

const sha512 = createSha512Base64(installerPath);
const latest = fs.readFileSync(latestPath, 'utf8');
expect(latest.match(/^sha512: .+$/gm)).toEqual([`sha512: ${sha512}`]);
expect(latest.match(/^\s+sha512: .+$/gm)).toEqual([` sha512: ${sha512}`]);
});

it('does nothing when latest.yml is missing', () => {
const distDir = createTempDir();
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
fs.writeFileSync(installerPath, 'signed installer bytes');

expect(() => updateLatestYmlForSignedInstaller(installerPath)).not.toThrow();
});

it('updates the installer referenced by latest.yml', () => {
const distDir = createTempDir();
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
const latestPath = path.join(distDir, 'latest.yml');
fs.writeFileSync(installerPath, 'signed installer bytes');
fs.writeFileSync(
latestPath,
[
'version: 0.1.1',
'files:',
' - url: Switchify-PC-Setup-0.1.1-x64.exe',
' sha512: old-file-checksum',
'path: Switchify-PC-Setup-0.1.1-x64.exe',
'sha512: old-path-checksum'
].join('\n')
);

updateLatestYmlForReferencedInstaller(latestPath);

const sha512 = createSha512Base64(installerPath);
const latest = fs.readFileSync(latestPath, 'utf8');
expect(latest.match(/^sha512: .+$/gm)).toEqual([`sha512: ${sha512}`]);
expect(latest.match(/^\s+sha512: .+$/gm)).toEqual([` sha512: ${sha512}`]);
});
});

function createTempDir(): string {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'switchify-sign-test-'));
tempDirs.push(tempDir);
return tempDir;
}