diff --git a/client/src/templates/Challenges/exam-download/show.tsx b/client/src/templates/Challenges/exam-download/show.tsx index 5e90d129bdd..34675e33066 100644 --- a/client/src/templates/Challenges/exam-download/show.tsx +++ b/client/src/templates/Challenges/exam-download/show.tsx @@ -11,7 +11,6 @@ import { Row, Col } from '@freecodecamp/ui'; -import { isEmpty } from 'lodash'; import { useTranslation, withTranslation } from 'react-i18next'; import { createSelector } from 'reselect'; import { connect } from 'react-redux'; @@ -108,50 +107,72 @@ function ShowExamDownload({ const { t } = useTranslation(); function handleDownloadLink(downloadLinks: string[]) { - const win = downloadLinks.find(link => link.match(/\.exe/)); - const macARM = downloadLinks.find( - link => link.match(/aarch64/) && link.match(/\.dmg/) - ); - const macX64 = downloadLinks.find( - link => link.match(/x64/) && link.match(/\.dmg/) - ); + // Filter out signature and metadata files first + const filtered = downloadLinks.filter(link => !/\.(sig|json)$/i.test(link)); - const linuxARM = downloadLinks.find( - link => link.match(/aarch64/) && link.match(/tar\.gz/) - ); + function normalizeArch(token: string): string { + if (!token) return ''; + const t = token.toLowerCase(); + if (/aarch64|arm64|arm/i.test(t)) return 'arm'; + if (/x86_64|x64|amd64/i.test(t)) return 'x64'; + if (/x86|i386|i686/i.test(t)) return 'x86'; + return t; + } - const linuxX64 = downloadLinks.find( - link => link.match(/amd64/) && link.match(/AppImage/) - ); + const items = filtered.map(link => { + const urlEnd = link.split('/').pop() ?? ''; + const name = urlEnd; + let ext = ''; + if (name.endsWith('.app.tar.gz')) ext = '.app.tar.gz'; + else if (name.endsWith('.tar.gz')) ext = '.tar.gz'; + else if (name.endsWith('.AppImage')) ext = '.AppImage'; + else if (name.endsWith('.dmg')) ext = '.dmg'; + else if (name.endsWith('.exe')) ext = '.exe'; + else { + const m = name.match(/(\.[^./]+)$/); + ext = m ? m[0] : ''; + } + + const archMatch = name.match( + /(aarch64|arm64|amd64|x86_64|x64|x86|i386)/i + ); + const archToken = archMatch ? normalizeArch(archMatch[0]) : ''; + + return { link, name, ext, arch: archToken }; + }); + + const detectedArch = normalizeArch(os.architecture || ''); + + function pickByExts(exts: string[], preferArch?: string) { + // prefer both ext + arch + let found = items.find( + it => exts.includes(it.ext) && it.arch === preferArch + ); + if (found) return found.link; + // then any with ext and unspecified arch + found = items.find( + it => exts.includes(it.ext) && (!preferArch || it.arch === '') + ); + if (found) return found.link; + // then any with ext + found = items.find(it => exts.includes(it.ext)); + return found ? found.link : ''; + } if (os.os === 'WIN') { - if (isEmpty(win)) return ''; - - return win; + return pickByExts(['.exe'], detectedArch) || ''; } if (os.os === 'MAC') { - if (os.architecture.toLowerCase() === 'arm') { - if (isEmpty(macARM)) return ''; - - return macARM; - } else { - if (isEmpty(macX64)) return ''; - - return macX64; - } + // prefer .dmg files + return pickByExts(['.dmg'], detectedArch) || ''; } if (os.os === 'LINUX') { - if (os.architecture.toLowerCase() === 'arm') { - if (isEmpty(linuxARM)) return ''; - - return linuxARM; - } else { - if (isEmpty(linuxX64)) return ''; - - return linuxX64; - } + // prefer AppImage, then .app.tar.gz, then .tar.gz + return ( + pickByExts(['.AppImage', '.app.tar.gz', '.tar.gz'], detectedArch) || '' + ); } return ''; @@ -337,31 +358,22 @@ function getRecommendedOs({ arch: string; ext: string; }): string { - switch (arch) { - case 'x64': - switch (ext) { - case '.dmg': - return 'x64 MacOS'; - case '.AppImage': - case '.app.tar.gz': - return 'x64 Linux'; - default: - return 'x64 Windows'; - } - case 'aarch64': - switch (ext) { - case '.dmg': - return 'ARM MacOS'; - case '.app.tar.gz': - return 'ARM Linux'; - default: - return 'ARM Windows'; - } - case 'amd64': - return 'x64 Linux'; - default: - return ''; - } + const osToExt = { + MacOS: ['.dmg', '.app', '.app.tar.gz'], + Linux: ['.deb', '.rpm', '.AppImage', '.tar.gz', '.AppImage.tar.gz'], + Windows: ['.exe', '.msi'] + } as const; + const archToHuman: Record = { + x64: '64-bit', + aarch64: 'ARM', + amd64: '64-bit', + i386: '32-bit', + x86: '32-bit' + }; + + const os = Object.entries(osToExt).find(([_, exts]) => exts.includes(ext)); + + return `${archToHuman[arch] ?? arch} ${os ? os[0] : ''}`; } function getLatest(releases: GitProps[]): GitProps {