From f4d86fccfd0e73e94c8c6e400a33514900bc5abe Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Wed, 30 Aug 2023 13:34:30 +0200 Subject: [PATCH] Fix Windows artifact naming in desktop packaging - Fix the naming convention in Electron output to align with previous artifact naming to not break external/internal URLs. - In desktop execution tests, make artifact locator logic stricter to test regression. --- electron-builder.yml | 2 +- .../extractors/common/app-artifact-locator.ts | 46 +++++++++++++++++++ .../{ => common}/extraction-result.ts | 0 .../app/extractors/linux.ts | 11 +++-- .../app/extractors/macos.ts | 13 ++++-- .../app/extractors/windows.ts | 24 +++++++--- scripts/check-desktop-runtime-errors/main.ts | 8 ++-- .../check-desktop-runtime-errors/utils/io.ts | 33 ------------- .../check-desktop-runtime-errors/utils/npm.ts | 11 ++++- 9 files changed, 95 insertions(+), 53 deletions(-) create mode 100644 scripts/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts rename scripts/check-desktop-runtime-errors/app/extractors/{ => common}/extraction-result.ts (100%) diff --git a/electron-builder.yml b/electron-builder.yml index a4037b78..dac7d78d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -4,7 +4,7 @@ win: target: nsis nsis: - artifactName: ${name}-${version}-Setup.${ext} + artifactName: ${name}-Setup-${version}.${ext} # ----- # Linux diff --git a/scripts/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts b/scripts/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts new file mode 100644 index 00000000..7d0c20e3 --- /dev/null +++ b/scripts/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts @@ -0,0 +1,46 @@ +import { join } from 'path'; +import { readdir } from 'fs/promises'; +import { die } from '../../../utils/log'; +import { exists } from '../../../utils/io'; +import { getAppName } from '../../../utils/npm'; + +export async function findByFilePattern( + pattern: string, + directory: string, + projectRootDir: string, +): Promise { + if (!directory) { throw new Error('Missing directory'); } + if (!pattern) { throw new Error('Missing file pattern'); } + + if (!await exists(directory)) { + return die(`Directory does not exist: ${directory}`); + } + + const directoryContents = await readdir(directory); + const appName = await getAppName(projectRootDir); + const regexPattern = pattern + /* eslint-disable no-template-curly-in-string */ + .replaceAll('${name}', escapeRegExp(appName)) + .replaceAll('${version}', '\\d+\\.\\d+\\.\\d+') + .replaceAll('${ext}', '.*'); + /* eslint-enable no-template-curly-in-string */ + const regex = new RegExp(`^${regexPattern}$`); + const foundFileNames = directoryContents.filter((file) => regex.test(file)); + if (!foundFileNames.length) { + return die(`No files found matching pattern "${pattern}" in ${directory} directory.`); + } + if (foundFileNames.length > 1) { + return die(`Found multiple files matching pattern "${pattern}": ${foundFileNames.join(', ')}`); + } + return { + absolutePath: join(directory, foundFileNames[0]), + }; +} + +function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +interface ArtifactLocation { + readonly absolutePath?: string; +} diff --git a/scripts/check-desktop-runtime-errors/app/extractors/extraction-result.ts b/scripts/check-desktop-runtime-errors/app/extractors/common/extraction-result.ts similarity index 100% rename from scripts/check-desktop-runtime-errors/app/extractors/extraction-result.ts rename to scripts/check-desktop-runtime-errors/app/extractors/common/extraction-result.ts diff --git a/scripts/check-desktop-runtime-errors/app/extractors/linux.ts b/scripts/check-desktop-runtime-errors/app/extractors/linux.ts index dd0b60e9..cfdd250e 100644 --- a/scripts/check-desktop-runtime-errors/app/extractors/linux.ts +++ b/scripts/check-desktop-runtime-errors/app/extractors/linux.ts @@ -1,15 +1,18 @@ import { access, chmod } from 'fs/promises'; import { constants } from 'fs'; -import { findSingleFileByExtension } from '../../utils/io'; import { log } from '../../utils/log'; -import { ExtractionResult } from './extraction-result'; +import { ExtractionResult } from './common/extraction-result'; +import { findByFilePattern } from './common/app-artifact-locator'; export async function prepareLinuxApp( desktopDistPath: string, + projectRootDir: string, ): Promise { - const { absolutePath: appFile } = await findSingleFileByExtension( - 'AppImage', + const { absolutePath: appFile } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}-${version}.AppImage', desktopDistPath, + projectRootDir, ); await makeExecutable(appFile); return { diff --git a/scripts/check-desktop-runtime-errors/app/extractors/macos.ts b/scripts/check-desktop-runtime-errors/app/extractors/macos.ts index a1f538bf..72d44da1 100644 --- a/scripts/check-desktop-runtime-errors/app/extractors/macos.ts +++ b/scripts/check-desktop-runtime-errors/app/extractors/macos.ts @@ -1,13 +1,20 @@ import { runCommand } from '../../utils/run-command'; -import { findSingleFileByExtension, exists } from '../../utils/io'; +import { exists } from '../../utils/io'; import { log, die, LogLevel } from '../../utils/log'; import { sleep } from '../../utils/sleep'; -import { ExtractionResult } from './extraction-result'; +import { ExtractionResult } from './common/extraction-result'; +import { findByFilePattern } from './common/app-artifact-locator'; export async function prepareMacOsApp( desktopDistPath: string, + projectRootDir: string, ): Promise { - const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath); + const { absolutePath: dmgPath } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}-${version}.dmg', + desktopDistPath, + projectRootDir, + ); const { mountPath } = await mountDmg(dmgPath); const appPath = await findMacAppExecutablePath(mountPath); return { diff --git a/scripts/check-desktop-runtime-errors/app/extractors/windows.ts b/scripts/check-desktop-runtime-errors/app/extractors/windows.ts index 7da059b5..e93ae96c 100644 --- a/scripts/check-desktop-runtime-errors/app/extractors/windows.ts +++ b/scripts/check-desktop-runtime-errors/app/extractors/windows.ts @@ -1,20 +1,22 @@ import { mkdtemp, rm } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; -import { findSingleFileByExtension, exists } from '../../utils/io'; +import { exists } from '../../utils/io'; import { log, die, LogLevel } from '../../utils/log'; import { runCommand } from '../../utils/run-command'; -import { ExtractionResult } from './extraction-result'; +import { ExtractionResult } from './common/extraction-result'; +import { findByFilePattern } from './common/app-artifact-locator'; export async function prepareWindowsApp( desktopDistPath: string, + projectRootDir: string, ): Promise { const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-')); if (await exists(workdir)) { log(`Temporary directory ${workdir} already exists, cleaning up...`); await rm(workdir, { recursive: true }); } - const appExecutablePath = await installNsis(workdir, desktopDistPath); + const appExecutablePath = await installNsis(workdir, desktopDistPath, projectRootDir); return { appExecutablePath, cleanup: async () => { @@ -31,16 +33,26 @@ export async function prepareWindowsApp( async function installNsis( installationPath: string, desktopDistPath: string, + projectRootDir: string, ): Promise { - const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath); - + const { absolutePath: installerPath } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}-Setup-${version}.exe', + desktopDistPath, + projectRootDir, + ); log(`Silently installing contents of ${installerPath} to ${installationPath}...`); const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`); if (error) { return die(`Failed to install.\n${error}`); } - const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath); + const { absolutePath: appExecutablePath } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}.exe', + installationPath, + projectRootDir, + ); return appExecutablePath; } diff --git a/scripts/check-desktop-runtime-errors/main.ts b/scripts/check-desktop-runtime-errors/main.ts index 8e425cb5..0865127d 100644 --- a/scripts/check-desktop-runtime-errors/main.ts +++ b/scripts/check-desktop-runtime-errors/main.ts @@ -16,7 +16,7 @@ import { SCREENSHOT_PATH, } from './config'; import { indentText } from './utils/text'; -import { ExtractionResult } from './app/extractors/extraction-result'; +import { ExtractionResult } from './app/extractors/common/extraction-result'; export async function main(): Promise { logCurrentArgs(); @@ -47,9 +47,9 @@ async function extractAndRun() { const extractors: { readonly [K in SupportedPlatform]: () => Promise; } = { - [SupportedPlatform.macOS]: () => prepareMacOsApp(DESKTOP_DIST_PATH), - [SupportedPlatform.Linux]: () => prepareLinuxApp(DESKTOP_DIST_PATH), - [SupportedPlatform.Windows]: () => prepareWindowsApp(DESKTOP_DIST_PATH), + [SupportedPlatform.macOS]: () => prepareMacOsApp(DESKTOP_DIST_PATH, PROJECT_DIR), + [SupportedPlatform.Linux]: () => prepareLinuxApp(DESKTOP_DIST_PATH, PROJECT_DIR), + [SupportedPlatform.Windows]: () => prepareWindowsApp(DESKTOP_DIST_PATH, PROJECT_DIR), }; const extractor = extractors[CURRENT_PLATFORM]; if (!extractor) { diff --git a/scripts/check-desktop-runtime-errors/utils/io.ts b/scripts/check-desktop-runtime-errors/utils/io.ts index 195d787e..de7b18dd 100644 --- a/scripts/check-desktop-runtime-errors/utils/io.ts +++ b/scripts/check-desktop-runtime-errors/utils/io.ts @@ -1,38 +1,5 @@ -import { extname, join } from 'path'; import { readdir, access } from 'fs/promises'; import { constants } from 'fs'; -import { log, die, LogLevel } from './log'; - -export async function findSingleFileByExtension( - extension: string, - directory: string, -): Promise { - if (!directory) { throw new Error('Missing directory'); } - if (!extension) { throw new Error('Missing file extension'); } - - if (!await exists(directory)) { - return die(`Directory does not exist: ${directory}`); - } - - const directoryContents = await readdir(directory); - const foundFileNames = directoryContents.filter((file) => extname(file) === `.${extension}`); - const withoutUninstaller = foundFileNames.filter( - (fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe` - ); - if (!withoutUninstaller.length) { - return die(`No ${extension} found in ${directory} directory.`); - } - if (withoutUninstaller.length > 1) { - log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LogLevel.Warn); - } - return { - absolutePath: join(directory, withoutUninstaller[0]), - }; -} - -interface FileSearchResult { - readonly absolutePath?: string; -} export async function exists(path: string): Promise { if (!path) { throw new Error('Missing path'); } diff --git a/scripts/check-desktop-runtime-errors/utils/npm.ts b/scripts/check-desktop-runtime-errors/utils/npm.ts index fec3a547..0ccec34d 100644 --- a/scripts/check-desktop-runtime-errors/utils/npm.ts +++ b/scripts/check-desktop-runtime-errors/utils/npm.ts @@ -64,15 +64,22 @@ export async function npmBuild( } } +const appNameCache = new Map(); + export async function getAppName(projectDir: string): Promise { if (!projectDir) { throw new Error('missing project directory'); } + if (appNameCache.has(projectDir)) { + return appNameCache.get(projectDir); + } const packageData = await readPackageJsonContents(projectDir); try { const packageJson = JSON.parse(packageData); - if (!packageJson.name) { + const name = packageJson.name as string; + if (!name) { return die(`The \`package.json\` file doesn't specify a name: ${packageData}`); } - return packageJson.name; + appNameCache.set(projectDir, name); + return name; } catch (error) { return die(`Unable to parse \`package.json\`. Error: ${error}\nContent: ${packageData}`); }