Skip to content

Commit 0da5fb7

Browse files
authored
Merge pull request #75 from beNative/codex/limit-windows-releases-to-2-files
Improve auto-update reliability and testing
2 parents 98dd4b8 + 25599cd commit 0da5fb7

File tree

6 files changed

+373
-187
lines changed

6 files changed

+373
-187
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules/
22
dist/
33
release/
4+
build-tests/
45
*.log
56
.DS_Store
67
npm-debug.log*

electron/autoUpdateHelpers.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import path from 'path';
2+
import type { UpdateDownloadedEvent } from 'electron-updater';
3+
4+
export type UpdaterArch = 'x64' | 'ia32' | 'arm64' | 'armv7l';
5+
6+
export type ArchDetectionResult = { arch: UpdaterArch | null; sources: string[] };
7+
8+
export const mapProcessArchToUpdaterArch = (arch: NodeJS.Process['arch']): UpdaterArch | null => {
9+
switch (arch) {
10+
case 'x64':
11+
case 'arm64':
12+
case 'ia32':
13+
return arch;
14+
case 'arm':
15+
return 'armv7l';
16+
default:
17+
return null;
18+
}
19+
};
20+
21+
export const detectArchFromFileName = (name: string | null | undefined): UpdaterArch | null => {
22+
if (!name) {
23+
return null;
24+
}
25+
const normalized = name.toLowerCase();
26+
const tokens = normalized.split(/[^a-z0-9]+/).filter(Boolean);
27+
const hasToken = (token: string) => tokens.includes(token);
28+
29+
if (hasToken('arm64') || hasToken('aarch64')) {
30+
return 'arm64';
31+
}
32+
if (hasToken('armv7l') || hasToken('armhf')) {
33+
return 'armv7l';
34+
}
35+
if (hasToken('ia32') || hasToken('x86') || hasToken('win32')) {
36+
return 'ia32';
37+
}
38+
if (hasToken('x64') || hasToken('amd64') || hasToken('win64')) {
39+
return 'x64';
40+
}
41+
42+
const endsWith32 = /(^|[^0-9])32($|[^0-9])/.test(normalized);
43+
const endsWith64 = /(^|[^0-9])64($|[^0-9])/.test(normalized);
44+
if (endsWith32 && !endsWith64) {
45+
return 'ia32';
46+
}
47+
if (endsWith64 && !endsWith32) {
48+
return 'x64';
49+
}
50+
return null;
51+
};
52+
53+
export const normalizeNameForComparison = (name: string): string => {
54+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
55+
};
56+
57+
export const getFileNameFromUrlLike = (input: string): string | null => {
58+
if (!input) {
59+
return null;
60+
}
61+
try {
62+
const parsed = new URL(input);
63+
return path.basename(parsed.pathname);
64+
} catch (error) {
65+
const sanitized = input.split('?')[0].split('#')[0];
66+
if (!sanitized) {
67+
return null;
68+
}
69+
return path.basename(sanitized);
70+
}
71+
};
72+
73+
export const determineDownloadedUpdateArch = (
74+
info: UpdateDownloadedEvent,
75+
downloadedName: string,
76+
processArch: NodeJS.Process['arch'] = process.arch,
77+
): ArchDetectionResult => {
78+
type ArchCandidate = { priority: number; sources: string[] };
79+
const candidates = new Map<UpdaterArch, ArchCandidate>();
80+
const ensureUnique = (values: string[]): string[] => {
81+
return Array.from(new Set(values));
82+
};
83+
const register = (arch: UpdaterArch | null, priority: number, source: string) => {
84+
if (!arch) {
85+
return;
86+
}
87+
const existing = candidates.get(arch);
88+
if (!existing) {
89+
candidates.set(arch, { priority, sources: [source] });
90+
return;
91+
}
92+
const mergedSources = ensureUnique([...existing.sources, source]);
93+
if (priority > existing.priority) {
94+
candidates.set(arch, { priority, sources: mergedSources });
95+
return;
96+
}
97+
existing.sources = mergedSources;
98+
};
99+
100+
register(mapProcessArchToUpdaterArch(processArch), 10, `process.arch:${processArch}`);
101+
register(detectArchFromFileName(downloadedName), 40, `downloadedName:${downloadedName}`);
102+
103+
if (typeof (info as any).path === 'string') {
104+
const legacyName = path.basename((info as any).path);
105+
register(detectArchFromFileName(legacyName), 50, `info.path:${legacyName}`);
106+
}
107+
108+
if (Array.isArray(info.files)) {
109+
for (const file of info.files) {
110+
if (typeof file?.url !== 'string') {
111+
continue;
112+
}
113+
const urlName = getFileNameFromUrlLike(file.url);
114+
const arch = detectArchFromFileName(urlName);
115+
const matchesDownloaded = urlName && normalizeNameForComparison(urlName) === normalizeNameForComparison(downloadedName);
116+
register(arch, matchesDownloaded ? 100 : 80, urlName ? `info.files:${urlName}` : 'info.files');
117+
}
118+
}
119+
120+
let selected: ArchDetectionResult = { arch: null, sources: [] };
121+
for (const [arch, candidate] of candidates.entries()) {
122+
const selectedPriority = selected.arch ? candidates.get(selected.arch)?.priority ?? -Infinity : -Infinity;
123+
if (!selected.arch || candidate.priority > selectedPriority) {
124+
selected = { arch, sources: [...candidate.sources] };
125+
}
126+
}
127+
return selected;
128+
};
129+
130+
export const filterNamesByArch = (names: string[], arch: UpdaterArch | null): string[] => {
131+
if (!arch) {
132+
return names;
133+
}
134+
const filtered = names.filter(name => detectArchFromFileName(name) === arch);
135+
return filtered.length > 0 ? filtered : names;
136+
};
137+
138+
export const extractCandidateNamesFromUpdateInfo = (
139+
info: UpdateDownloadedEvent,
140+
extension: string,
141+
): string[] => {
142+
const names = new Set<string>();
143+
const normalizedExt = extension.toLowerCase();
144+
const considerName = (name: string | null | undefined) => {
145+
if (!name) {
146+
return;
147+
}
148+
if (!normalizedExt || path.extname(name).toLowerCase() === normalizedExt) {
149+
names.add(name);
150+
}
151+
};
152+
153+
if (Array.isArray(info.files)) {
154+
for (const file of info.files) {
155+
if (typeof file?.url === 'string') {
156+
considerName(getFileNameFromUrlLike(file.url));
157+
}
158+
}
159+
}
160+
161+
if (typeof (info as any).path === 'string') {
162+
considerName(path.basename((info as any).path));
163+
}
164+
165+
if (typeof info.downloadedFile === 'string') {
166+
considerName(path.basename(info.downloadedFile));
167+
}
168+
169+
return Array.from(names);
170+
};
171+
172+
export type FileValidationSuccess = {
173+
success: true;
174+
filePath: string;
175+
expectedName: string;
176+
renamed?: boolean;
177+
officialNames: string[];
178+
};
179+
180+
export type FileValidationFailure = {
181+
success: false;
182+
error: string;
183+
downloadedName: string;
184+
officialNames: string[];
185+
};
186+
187+
export type FileValidationResult = FileValidationSuccess | FileValidationFailure;

0 commit comments

Comments
 (0)