diff --git a/.github/workflows/os-compatibility.yml b/.github/workflows/os-compatibility.yml index a9fa25b..d71d7d3 100644 --- a/.github/workflows/os-compatibility.yml +++ b/.github/workflows/os-compatibility.yml @@ -92,6 +92,42 @@ jobs: if: ${{ failure() }} uses: actions/upload-artifact@v4.3.5 with: - name: docker-fedora-${{ matrix.version }}-${{ matrix.version-requirement }} + name: docker-fedora-${{ matrix.version }}-${{ matrix.version-requirement }}-${{ matrix.ubuntu-version}} path: /tmp/mysqlmsn - compression-level: 0 \ No newline at end of file + compression-level: 0 + + alpine-docker: + runs-on: ubuntu-${{ matrix.ubuntu-version}} + + strategy: + fail-fast: false + matrix: + #There is no 10.0.0 at the time of writing, but since greater than characters are not allowed in GitHub Actions artifacts names, 9.0.1 - 10.0.0 will be used instead of >9.0.0 + version-requirement: [ '8.4.0 - 9.0.0', '9.0.1 - 10.0.0'] + ubuntu-version: [24.04, 24.04-arm] + + container: alpine:3.22 + + steps: + - name: Install required libraries for test setup + run: apk add git nodejs npm + + - name: Install required libraries for MySQL on alpine + run: apk add libaio libstdc++ + + - name: Checkout + run: git clone https://github.com/${{ github.repository }} --branch ${{ github.head_ref || github.ref_name }} --single-branch --depth 1 mysqlmsn + + - name: Install packages + working-directory: mysqlmsn + run: npm ci + + - name: Print available storage space + run: df -h + + - name: Run tests + working-directory: mysqlmsn + env: + VERSION_REQUIREMENT: ${{ matrix.version-requirement }} + X_OFF: true + run: npm run os-compat:ci \ No newline at end of file diff --git a/README.md b/README.md index ecd95f6..0c593e6 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ The name of the user to use to login to the database If on Windows, this is the name of the named pipe that MySQL is listening on. If not on Windows, this is the path to the socket that MySQL is listening on. - `xSocket: string` If on Windows, this is the name of the named pipe that the MySQL X Plugin is listening on. If not on Windows, this is the path that the MySQL X Plugin is listening on. If ```options.xEnabled``` is set to "OFF", this value will be an empty string. -- `mysql: {version: string, versionIsInstalledOnSystem: boolean, xPluginIsEnabled: boolean}` -An object with three properties. ```version``` is the version of MySQL used to create the database. ```versionIsInstalledOnSystem``` will be true if the MySQL version used is already installed on the system and false if the version had to be downloaded from MySQL's CDN. ```xPluginIsEnabled``` will be true if ```options.xEnabled``` is set to "FORCE", and will be false if ```options.xEnabled``` is set to "OFF". +- `mysql: {version: string, versionIsInstalledOnSystem: boolean}` +An object with three properties. ```version``` is the version of MySQL used to create the database. ```versionIsInstalledOnSystem``` will be true if the MySQL version used is already installed on the system and false if the version had to be downloaded from MySQL's CDN. - `stop: () => Promise` The method to stop the database. The returned promise resolves when the database has successfully stopped. @@ -194,4 +194,4 @@ Description: The MySQL binary architecture to execute. MySQL does not offer serv Default: "FORCE" -Description: This option follows the convention set out by the [MySQL Documentation](https://dev.mysql.com/doc/refman/en/plugin-loading.html). If set to "OFF", the MySQL X Plugin will not initialise. If set to "FORCE", the MySQL Server will not start up without a successful initialisation of the plugin, meaning that it's guaranteed the server will start up with MySQL X enabled. If the MySQL X initialisation fails, the server will not start up. +Description: This option follows the convention set out by the [MySQL Documentation](https://dev.mysql.com/doc/refman/en/plugin-loading.html). If set to "OFF", the MySQL X Plugin will not initialise. If set to "FORCE", the MySQL Server will either start up with the MySQL X Plugin guaranteed to have successfully initialised, or if initialisation fails, the server will fail to start up. diff --git a/dist/src/libraries/Version.js b/dist/src/libraries/Version.js index 67d31b7..e7c60c4 100644 --- a/dist/src/libraries/Version.js +++ b/dist/src/libraries/Version.js @@ -85,16 +85,26 @@ function getBinaryURL(versionToGet = "x", currentArch) { const minVersion = minVersions[0]; throw `Your operating system is too out of date to run a version of MySQL that fits the following requirement: ${versionToGet}. The oldest version for your operating system that you would need to get a version that satisfies the version requirement is ${minVersion} but your current operating system is ${coercedOSRelease.version}. Please try changing your MySQL version requirement, updating your OS to a newer version, or if you believe this is a bug, please report this on GitHub.`; } - if (process.platform === 'linux' && LinuxOSRelease_1.default.NAME === 'Ubuntu' && LinuxOSRelease_1.default.VERSION_ID >= '24.04') { - //Since Ubuntu >= 24.04 uses libaio1t64 instead of libaio, this package has to copy libaio1t64 into a folder that MySQL looks in for dynamically linked libraries with the filename "libaio.so.1". - //I have not been able to find a suitable folder for libaio1t64 to be copied into for MySQL < 8.0.4, so here we are filtering all versions lower than 8.0.4 since they fail to launch in Ubuntu 24.04. - //If there is a suitable filepath for libaio1t64 to be copied into for MySQL < 8.0.4 then this check can be removed and these older MySQL versions can run on Ubuntu. - //Pull requests are welcome for adding >= Ubuntu 24.04 support for MySQL < 8.0.4. - //A way to get MySQL running on Ubuntu >= 24.04 is to symlink libaio1t64 to the location libaio would be. It is not suitable for this package to be doing that automatically, so instead this package has been copying libaio1t64 into the MySQL binary folder. - selectedVersions = selectedVersions.filter(v => !(0, semver_1.lt)(v, '8.0.4')); - } - if (selectedVersions.length === 0) { - throw `You are running a version of Ubuntu that is too modern to run any MySQL versions with this package that match the following version requirement: ${versionToGet}. Please choose a newer version of MySQL to use, or if you believe this is a bug please report this on GitHub.`; + const isOnAlpineLinux = process.platform === 'linux' && LinuxOSRelease_1.default.NAME === 'Alpine Linux'; + if (process.platform === 'linux') { + if (LinuxOSRelease_1.default.NAME === 'Ubuntu' && LinuxOSRelease_1.default.VERSION_ID >= '24.04') { + //Since Ubuntu >= 24.04 uses libaio1t64 instead of libaio, this package has to copy libaio1t64 into a folder that MySQL looks in for dynamically linked libraries with the filename "libaio.so.1". + //I have not been able to find a suitable folder for libaio1t64 to be copied into for MySQL < 8.0.4, so here we are filtering all versions lower than 8.0.4 since they fail to launch in Ubuntu 24.04. + //If there is a suitable filepath for libaio1t64 to be copied into for MySQL < 8.0.4 then this check can be removed and these older MySQL versions can run on Ubuntu. + //Pull requests are welcome for adding >= Ubuntu 24.04 support for MySQL < 8.0.4. + //A way to get MySQL running on Ubuntu >= 24.04 is to symlink libaio1t64 to the location libaio would be. It is not suitable for this package to be doing that automatically, so instead this package has been copying libaio1t64 into the MySQL binary folder. + selectedVersions = selectedVersions.filter(v => !(0, semver_1.lt)(v, '8.0.4')); + if (selectedVersions.length === 0) { + throw `You are running a version of Ubuntu that is too modern to run any MySQL versions with this package that match the following version requirement: ${versionToGet}. Please choose a newer version of MySQL to use, or if you believe this is a bug please report this on GitHub.`; + } + } + else if (isOnAlpineLinux) { + //https://github.com/Sebastian-Webster/mysql-server-musl-binaries only has support for v8.4.x and 9.x binaries + selectedVersions = selectedVersions.filter(v => (0, semver_1.satisfies)(v, '8.4.x') || (0, semver_1.satisfies)(v, '9.x')); + if (selectedVersions.length === 0) { + throw 'mysql-memory-server has detected you are running this package on Alpine Linux. The source for MySQL with musl libc only provides binaries for MySQL 8.4.x and 9.x and as such only those versions can be used with this package. Please use 8.4.x or 9.x.'; + } + } } //Sorts versions in descending order selectedVersions.sort((a, b) => a < b ? 1 : -1); @@ -110,6 +120,9 @@ function getBinaryURL(versionToGet = "x", currentArch) { const macOSVersionNameKey = MySQLmacOSVersionNameKeys.find(range => (0, semver_1.satisfies)(selectedVersion, range)); fileLocation = `${(0, semver_1.major)(selectedVersion)}.${(0, semver_1.minor)(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-${constants_1.MYSQL_MACOS_VERSIONS_IN_FILENAME[macOSVersionNameKey]}-${currentArch === 'x64' ? 'x86_64' : 'arm64'}.tar.gz`; } + else if (isOnAlpineLinux) { + fileLocation = `https://github.com/Sebastian-Webster/mysql-server-musl-binaries/releases/download/current/mysql-musl-${selectedVersion}-${currentArch === 'x64' ? 'x86_64' : 'arm64'}.tar.gz`; + } else if (currentOS === 'linux') { const glibcObject = constants_1.MYSQL_LINUX_GLIBC_VERSIONS[currentArch]; const glibcVersionKeys = Object.keys(glibcObject); @@ -124,8 +137,12 @@ function getBinaryURL(versionToGet = "x", currentArch) { const fileExtension = constants_1.MYSQL_LINUX_FILE_EXTENSIONS[currentArch][fileExtensionKey]; fileLocation = `${(0, semver_1.major)(selectedVersion)}.${(0, semver_1.minor)(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-linux-${minimalInstallAvailable !== 'no-glibc-tag' ? `glibc${glibcVersion}-` : ''}${currentArch === 'x64' ? 'x86_64' : 'aarch64'}${minimalInstallAvailable !== 'no' && (process.arch !== 'arm64' ? true : (0, semver_1.satisfies)(selectedVersion, constants_1.MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE_ARM64)) ? `-minimal${(0, semver_1.satisfies)(selectedVersion, constants_1.MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS) ? '-rebuild' : ''}` : ''}.tar.${fileExtension}`; } + else { + throw 'You are running this package on an unsupported OS. Please use either Windows, macOS, or a Linux-based OS.'; + } return { version: selectedVersion, - url: constants_1.archiveBaseURL + fileLocation + url: isOnAlpineLinux ? fileLocation : (constants_1.archiveBaseURL + fileLocation), + hostedByOracle: !isOnAlpineLinux // Only the Alpine Linux binaries are not hosted on the MySQL CDN. }; } diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index f5497f3..bae429f 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -58,6 +58,7 @@ export type DownloadedMySQLVersion = { export type BinaryInfo = { url: string; version: string; + hostedByOracle: boolean; }; export type OptionTypeChecks = { [key in keyof Required]: { diff --git a/docs/SUPPORTED_MYSQL_DOWNLOADS.md b/docs/SUPPORTED_MYSQL_DOWNLOADS.md index 7c1fdc6..74f8d63 100644 --- a/docs/SUPPORTED_MYSQL_DOWNLOADS.md +++ b/docs/SUPPORTED_MYSQL_DOWNLOADS.md @@ -8,7 +8,7 @@ - Windows - Linux -*```mysql-memory-server``` gets tested on Ubuntu 22.04 (x64 and arm64) and 24.04 (x64 and arm64), Fedora 41 (x64 and arm64) and 42 (x64 and arm64), macOS 13 (x64), 14 (arm64), and 15 (arm64), Windows 11 (arm64), and Windows Server 2022 (x64) and 2025 (x64). Linux distributions and Windows and macOS versions other than the ones tested may or may not work and are not guaranteed to work with this package.* +*```mysql-memory-server``` gets tested on Ubuntu 22.04 (x64 and arm64) and 24.04 (x64 and arm64), Fedora 41 (x64 and arm64) and 42 (x64 and arm64), Alpine 3.22 (x64 and arm64), macOS 13 (x64), 14 (arm64), and 15 (arm64), Windows 11 (arm64), and Windows Server 2022 (x64) and 2025 (x64). Linux distributions and Windows and macOS versions other than the ones tested may or may not work and are not guaranteed to work with this package.* ## Binaries not available for download @@ -16,6 +16,10 @@ - Versions 5.7.32 - 5.7.44 are not available for download only for macOS systems as MySQL stopped supporting macOS Mojave starting from 5.7.32 for the rest of the 5.7.x line. As a result, those versions are not available for macOS in the MySQL CDN. +## Alpine Linux Limitations + +Only MySQL versions 8.4.x and 9.x can be downloaded and ran with this package on Alpine Linux. The binaries for Alpine Linux are sourced from [Sebastian-Webster/mysql-server-musl-binaries](https://github.com/Sebastian-Webster/mysql-server-musl-binaries) on GitHub as Oracle does not support MySQL on musl-based Linux distributions. That repository only has support for MySQL 8.4.x and 9.x. If you discover any issues with MySQL (and not this package) when you are running this on Alpine Linux, please report the issue on that repository and not the ```mysql-memory-server``` one. The MySQL X Plugin is also not available when running on Alpine Linux due to compilation errors for musl. + ## Native Binary Architectures *Architectures used can be overridden by the ```arch``` option provided your OS and system supports running applications that use those architectures.* @@ -56,7 +60,7 @@ Windows - No documented maximum version macOS - No documented maximum version -Fedora Linux - No documented maximum version +Fedora & Alpine Linux - No documented maximum version Ubuntu Linux: @@ -86,4 +90,6 @@ Ubuntu Linux: Fedora Linux: ```libaio1``` package and ```tar``` package -*Document last updated in v1.11.0* \ No newline at end of file +Alpine Linux: ```libstdc++``` package and ```libaio``` package + +*Document last updated in v1.12.0* \ No newline at end of file diff --git a/package.json b/package.json index eb3e229..fa9c7e7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "scripts": { "test": "jest --verbose --colors", "test:ci": "jest tests/ci --setupFilesAfterEnv ./ciSetup.js --verbose --colors", - "os-compat:ci": "jest --setupFilesAfterEnv ./ciSetup.js --verbose --colors --runTestsByPath tests/versions.test.ts" + "os-compat:ci": "jest tests/versions.test.ts --setupFilesAfterEnv ./ciSetup.js --verbose --colors" }, "engines": { "node": ">=16.6.0", diff --git a/src/constants.ts b/src/constants.ts index f7a1e41..8cf98c9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -122,8 +122,8 @@ export const OPTION_TYPE_CHECKS: OptionTypeChecks = { } as const; export const MIN_SUPPORTED_MYSQL = '5.7.19'; -export const downloadsBaseURL = 'https://cdn.mysql.com//Downloads/MySQL-' -export const archiveBaseURL = 'https://cdn.mysql.com/archives/mysql-' +export const MySQLCDNDownloadsBaseURL = 'https://cdn.mysql.com//Downloads/MySQL-' +export const MySQLCDNArchivesBaseURL = 'https://cdn.mysql.com/archives/mysql-' // Versions 8.0.29, 8.0.38, 8.4.1, and 9.0.0 have been purposefully left out of this list as MySQL has removed them from the CDN due to critical issues. export const DOWNLOADABLE_MYSQL_VERSIONS = [ '5.7.19', '5.7.20', '5.7.21', '5.7.22', '5.7.23', '5.7.24', '5.7.25', '5.7.26', '5.7.27', '5.7.28', '5.7.29', '5.7.30', '5.7.31', '5.7.32', '5.7.33', '5.7.34', '5.7.35', '5.7.36', '5.7.37', '5.7.38', '5.7.39', '5.7.40', '5.7.41', '5.7.42', '5.7.43', '5.7.44', diff --git a/src/index.ts b/src/index.ts index b4872d6..8557c16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { BinaryInfo, InternalServerOptions, ServerOptions } from '../types' import getBinaryURL from './libraries/Version' import { downloadBinary } from './libraries/Downloader' import { MIN_SUPPORTED_MYSQL, DEFAULT_OPTIONS_KEYS, OPTION_TYPE_CHECKS, DEFAULT_OPTIONS } from './constants' +import etcOSRelease from './libraries/LinuxOSRelease' export async function createDB(opts?: ServerOptions) { const suppliedOpts = opts || {}; @@ -41,7 +42,7 @@ export async function createDB(opts?: ServerOptions) { throw `A version of MySQL is installed on your system that is not supported by this package. If you want to download a MySQL binary instead of getting this error, please set the option "ignoreUnsupportedSystemVersion" to true.` } - logger.log('Version currently installed:', version) + logger.log('Version currently installed:', version, 'Platform:', process.platform, 'etcOSRelease:', etcOSRelease) if (version === null || (options.version && !satisfies(version.version, options.version)) || unsupportedMySQLIsInstalled) { let binaryInfo: BinaryInfo; let binaryFilepath: string; @@ -55,7 +56,7 @@ export async function createDB(opts?: ServerOptions) { } logger.log('Running downloaded binary') - return await executor.startMySQL(options, {path: binaryFilepath, version: binaryInfo.version, installedOnSystem: false}) + return await executor.startMySQL(options, {path: binaryFilepath, version: binaryInfo.version, installedOnSystem: false, xPluginSupported: binaryInfo.xPluginSupported}) } else { logger.log(version) return await executor.startMySQL(options, version) diff --git a/src/libraries/Downloader.ts b/src/libraries/Downloader.ts index 4dc5a60..a5ccfa2 100644 --- a/src/libraries/Downloader.ts +++ b/src/libraries/Downloader.ts @@ -8,7 +8,7 @@ import { randomUUID } from 'crypto'; import { execFile } from 'child_process'; import { BinaryInfo, InternalServerOptions } from '../../types'; import { lockFile, waitForLock } from './FileLock'; -import { archiveBaseURL, downloadsBaseURL, getInternalEnvVariable } from '../constants'; +import { getInternalEnvVariable, MySQLCDNArchivesBaseURL, MySQLCDNDownloadsBaseURL } from '../constants'; function handleTarExtraction(filepath: string, extractedPath: string): Promise { return new Promise((resolve, reject) => { @@ -21,6 +21,43 @@ function handleTarExtraction(filepath: string, extractedPath: string): Promise { + const options: https.RequestOptions = { + headers: { + 'accept': '*/*', + 'connection': 'keep-alive' + } + } + + return new Promise((resolve, reject) => { + const request = https.get(url, options, response => { + const statusCode = response.statusCode + const location = response.headers.location + + if (statusCode !== 302) { + request.destroy(); + + reject(`Received status code ${statusCode} while getting redirect URL for binary download. Used URL: ${url}`) + return + } + + if (typeof location === 'string' && location.length > 0) { + request.destroy() + resolve(location) + return + } + + request.destroy() + reject(`Received incorrect URL information. Expected a non-empty string. Received: ${JSON.stringify(location)}`) + }) + + request.on('error', (err) => { + request.destroy(); + reject(err.message) + }) + }) +} + function downloadFromCDN(url: string, downloadLocation: string, logger: Logger): Promise { return new Promise(async (resolve, reject) => { if (fs.existsSync(downloadLocation)) { @@ -118,7 +155,7 @@ function promisifiedZipExtraction(archiveLocation: string, extractedLocation: st }) } -function extractBinary(url: string, archiveLocation: string, extractedLocation: string, logger: Logger): Promise { +function extractBinary(url: string, archiveLocation: string, extractedLocation: string, binaryInfo: BinaryInfo, logger: Logger): Promise { return new Promise(async (resolve, reject) => { if (fs.existsSync(extractedLocation)) { logger.warn('Removing item at extractedLocation:', extractedLocation, 'so the MySQL binary can be stored there. This is probably because a previous download/extraction failed.') @@ -130,12 +167,18 @@ function extractBinary(url: string, archiveLocation: string, extractedLocation: await fsPromises.mkdir(extractedLocation, {recursive: true}) - const splitURL = url.split('/') - const mySQLFolderName = splitURL[splitURL.length - 1] - if (!mySQLFolderName) { - return reject(`Folder name is undefined for url: ${url}`) + let folderName = '' + + if (binaryInfo.hostedByOracle) { + const splitURL = url.split('/') + const mySQLFolderName = splitURL[splitURL.length - 1] + if (!mySQLFolderName) { + return reject(`Folder name is undefined for url: ${url}`) + } + folderName = mySQLFolderName.replace(`.${fileExtension}`, '') + } else { + folderName = `mysql-${binaryInfo.version}` } - const folderName = mySQLFolderName.replace(`.${fileExtension}`, '') let extractionError: any = undefined; @@ -249,13 +292,13 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp let useDownloadsURL = false; do { + const downloadURL = binaryInfo.hostedByOracle ? `${useDownloadsURL ? MySQLCDNDownloadsBaseURL : MySQLCDNArchivesBaseURL}${url}` : await getFileDownloadURLRedirect(url) try { downloadTries++; - const downloadURL = useDownloadsURL ? url.replace(archiveBaseURL, downloadsBaseURL) : url logger.log(`Starting download for MySQL version ${version} from ${downloadURL}.`) await downloadFromCDN(downloadURL, archivePath, logger) logger.log(`Finished downloading MySQL version ${version} from ${downloadURL}. Now starting binary extraction.`) - await extractBinary(downloadURL, archivePath, extractedPath, logger) + await extractBinary(downloadURL, archivePath, extractedPath, binaryInfo, logger) logger.log(`Finished extraction for version ${version}`) break } catch (e) { @@ -269,21 +312,21 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp logger.error('An error occurred while deleting extractedPath and/or archivePath:', e) } - if (e?.includes?.('status code 404')) { - if (!useDownloadsURL) { - //Retry with downloads URL - downloadTries--; + // If we got a 404 error while downloading a binary from Oracle and have not retried with the Downloads URL yet + // then retry with the Downloads URL. Otherwise, reject. + if (e?.includes('status code 404')) { + if (binaryInfo.hostedByOracle && useDownloadsURL === false) { useDownloadsURL = true; - logger.log(`Encountered error 404 when using archives URL for version ${version}. Now retrying with the downloads URL.`) - continue; + downloadTries-- + continue } else { try { await releaseFunction() } catch (e) { - logger.error('An error occurred while releasing lock after receiving a 404 error on both downloads and archives URLs. The error was:', e) + logger.error('An error occurred while releasing lock after receiving a 404 error on download. The error was:', e) + } finally { + return reject(`Binary download for MySQL version ${binaryInfo.version} returned status code 404 at URL ${downloadURL}. Aborting download.`) } - - return reject(`Both URLs for MySQL version ${binaryInfo.version} returned status code 404. Aborting download.`) } } @@ -314,6 +357,7 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp let useDownloadsURL = false; do { + const downloadURL = binaryInfo.hostedByOracle ? `${useDownloadsURL ? MySQLCDNDownloadsBaseURL : MySQLCDNArchivesBaseURL}${url}` : await getFileDownloadURLRedirect(url) const uuid = randomUUID() const zipFilepath = `${dirpath}/${uuid}.${fileExtension}` logger.log('Binary filepath:', zipFilepath) @@ -321,11 +365,10 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp try { downloadTries++ - const downloadURL = useDownloadsURL ? url.replace(archiveBaseURL, downloadsBaseURL) : url logger.log(`Starting download for MySQL version ${version} from ${downloadURL}.`) await downloadFromCDN(downloadURL, zipFilepath, logger) logger.log(`Finished downloading MySQL version ${version} from ${downloadURL}. Now starting binary extraction.`) - const binaryPath = await extractBinary(downloadURL, zipFilepath, extractedPath, logger) + const binaryPath = await extractBinary(downloadURL, zipFilepath, extractedPath, binaryInfo, logger) logger.log(`Finished extraction for version ${version}`) return resolve(binaryPath) } catch (e) { @@ -339,15 +382,15 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp logger.error('An error occurred while deleting extractedPath and/or archivePath:', e) } - if (e?.includes?.('status code 404')) { - if (!useDownloadsURL) { - //Retry with downloads URL - downloadTries--; + // If we got a 404 error while downloading a binary from Oracle and have not retried with the Downloads URL yet + // then retry with the Downloads URL. Otherwise, reject. + if (e?.includes('status code 404')) { + if (binaryInfo.hostedByOracle && useDownloadsURL === false) { useDownloadsURL = true; - logger.log(`Encountered error 404 when using archives URL for version ${version}. Now retrying with the downloads URL.`) - continue; + downloadTries-- + continue } else { - return reject(`Both URLs for MySQL version ${binaryInfo.version} returned status code 404. Aborting download.`) + return reject(`Binary download for MySQL version ${binaryInfo.version} returned status code 404 at URL ${downloadURL}. Aborting download.`) } } diff --git a/src/libraries/Executor.ts b/src/libraries/Executor.ts index be3f3c5..24dd4a6 100644 --- a/src/libraries/Executor.ts +++ b/src/libraries/Executor.ts @@ -11,7 +11,7 @@ import { lockFile, waitForLock } from "./FileLock"; import { onExit } from "signal-exit"; import { randomUUID } from "crypto"; import { getInternalEnvVariable } from "../constants"; -import etcOSRelease from "./LinuxOSRelease"; +import etcOSRelease, { isOnAlpineLinux } from "./LinuxOSRelease"; class Executor { logger: Logger; @@ -19,6 +19,7 @@ class Executor { removeExitHandler: () => void; version: string; versionInstalledOnSystem: boolean; + versionSupportsMySQLX: boolean; databasePath: string killedFromPortIssue: boolean; @@ -79,7 +80,6 @@ class Executor { const mysqlArguments = [ '--no-defaults', - `--mysqlx=${options.xEnabled}`, `--port=${port}`, `--datadir=${datadir}`, `--socket=${socket}`, @@ -92,6 +92,10 @@ class Executor { `--user=${os.userInfo().username}` ] + if (this.versionSupportsMySQLX) { + mysqlArguments.push(`--mysqlx=${options.xEnabled}`) + } + if (options.xEnabled !== 'OFF') { mysqlArguments.push(`--mysqlx-port=${mySQLXPort}`) mysqlArguments.push(`--mysqlx-socket=${xSocket}`) @@ -225,29 +229,16 @@ class Executor { return //Promise rejection will be handled in the process.on('close') section because this.killedFromPortIssue is being set to true } - const xStartedSuccessfully = file.includes('X Plugin ready for connections') || file.includes("mysqlx reported: 'Server starts handling incoming connections'") || (lte(this.version, '8.0.12') && gte(this.version, '8.0.4') && file.search(/\[ERROR\].*Plugin mysqlx reported/m) === -1) - - if (options.xEnabled === 'FORCE' && !xStartedSuccessfully) { - this.logger.error('Error file:', file) - this.logger.error('MySQL X failed to start successfully and xEnabled is set to "FORCE". Error log is above this message. If this is happening continually and you can start the database without the X Plugin, you can set options.xEnabled to "OFF" instead of "FORCE".') - const killed = await this.#killProcess(process) - if (!killed) { - this.logger.error('Failed to kill MySQL process after MySQL X failing to initialise.') - } - return reject('X Plugin failed to start and options.xEnabled is set to "FORCE".') - } - const result: MySQLDB = { port, - xPort: xStartedSuccessfully ? mySQLXPort : -1, + xPort: options.xEnabled === 'FORCE' ? mySQLXPort : -1, socket, - xSocket: xStartedSuccessfully ? xSocket : '', + xSocket: options.xEnabled === 'FORCE' ? xSocket : '', dbName: options.dbName, username: options.username, mysql: { version: this.version, - versionIsInstalledOnSystem: this.versionInstalledOnSystem, - xPluginIsEnabled: xStartedSuccessfully + versionIsInstalledOnSystem: this.versionInstalledOnSystem }, stop: () => { return new Promise(async (resolve, reject) => { @@ -309,7 +300,7 @@ class Executor { if (version === null) { return reject('Could not get MySQL version') } else { - versions.push({version: version.version, path, installedOnSystem: true}) + versions.push({version: version.version, path, installedOnSystem: true, xPluginSupported: gte(version, '5.7.19')}) } } @@ -334,7 +325,7 @@ class Executor { if (version === null) { reject('Could not get installed MySQL version') } else { - resolve({version: version.version, path: 'mysqld', installedOnSystem: true}) + resolve({version: version.version, path: 'mysqld', installedOnSystem: true, xPluginSupported: gte(version, '5.7.19')}) } } } @@ -488,8 +479,16 @@ class Executor { } async startMySQL(options: InternalServerOptions, installedMySQLBinary: DownloadedMySQLVersion): Promise { + if (installedMySQLBinary.xPluginSupported === false && options.xEnabled === 'FORCE') { + if (isOnAlpineLinux) { + throw "options.xEnabled is set to 'FORCE'. You are running this package on Alpine Linux. The MySQL binaries for Alpine Linux do not support the MySQL X Plugin. If you don't want to use the MySQL X Plugin, please set options.xEnabled to 'OFF' and that will remove this error message. If you must have MySQL X enabled for each database creation, please use a different OS to run this package." + } + throw "options.xEnabled is set to 'FORCE'. The version of MySQL you are using does not support MySQL X. If you don't want to use the MySQL X Plugin, please set options.xEnabled to 'OFF'. Otherwise, if MySQL X is required, please use a different version of MySQL that supports the X plugin." + } + this.version = installedMySQLBinary.version this.versionInstalledOnSystem = installedMySQLBinary.installedOnSystem + this.versionSupportsMySQLX = installedMySQLBinary.xPluginSupported this.removeExitHandler = onExit(() => { if (getInternalEnvVariable('cli') === 'true') { console.log('\nShutting down the ephemeral MySQL database and cleaning all related files...') diff --git a/src/libraries/LinuxOSRelease.ts b/src/libraries/LinuxOSRelease.ts index 52bee71..78c1a48 100644 --- a/src/libraries/LinuxOSRelease.ts +++ b/src/libraries/LinuxOSRelease.ts @@ -1,7 +1,7 @@ import fs from 'fs' import { LinuxEtcOSRelease } from '../../types' -const releaseDetails = {} +const releaseDetails: LinuxEtcOSRelease = {} if (process.platform === 'linux') { const file = fs.readFileSync('/etc/os-release', 'utf8') @@ -14,4 +14,6 @@ if (process.platform === 'linux') { } } -export default releaseDetails as LinuxEtcOSRelease; \ No newline at end of file +export const isOnAlpineLinux = process.platform === 'linux' && releaseDetails?.ID === 'alpine' + +export default releaseDetails; \ No newline at end of file diff --git a/src/libraries/Version.ts b/src/libraries/Version.ts index 36bf56c..7e5ace2 100644 --- a/src/libraries/Version.ts +++ b/src/libraries/Version.ts @@ -1,8 +1,8 @@ import { BinaryInfo, JSRuntimeVersion } from "../../types"; import * as os from 'os' import { satisfies, coerce, lt, major, minor } from "semver"; -import { archiveBaseURL, DMR_MYSQL_VERSIONS, DOWNLOADABLE_MYSQL_VERSIONS, MYSQL_ARCH_SUPPORT, MYSQL_LINUX_FILE_EXTENSIONS, MYSQL_LINUX_GLIBC_VERSIONS, MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE, MYSQL_MACOS_VERSIONS_IN_FILENAME, MYSQL_MIN_OS_SUPPORT, RC_MYSQL_VERSIONS, MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS, MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE_ARM64 } from "../constants"; -import etcOSRelease from "./LinuxOSRelease"; +import { MySQLCDNDownloadsBaseURL, DMR_MYSQL_VERSIONS, DOWNLOADABLE_MYSQL_VERSIONS, MYSQL_ARCH_SUPPORT, MYSQL_LINUX_FILE_EXTENSIONS, MYSQL_LINUX_GLIBC_VERSIONS, MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE, MYSQL_MACOS_VERSIONS_IN_FILENAME, MYSQL_MIN_OS_SUPPORT, RC_MYSQL_VERSIONS, MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS, MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE_ARM64 } from "../constants"; +import etcOSRelease, { isOnAlpineLinux } from "./LinuxOSRelease"; export default function getBinaryURL(versionToGet: string = "x", currentArch: string): BinaryInfo { let selectedVersions = DOWNLOADABLE_MYSQL_VERSIONS.filter(version => satisfies(version, versionToGet)); @@ -59,17 +59,26 @@ export default function getBinaryURL(versionToGet: string = "x", currentArch: st throw `Your operating system is too out of date to run a version of MySQL that fits the following requirement: ${versionToGet}. The oldest version for your operating system that you would need to get a version that satisfies the version requirement is ${minVersion} but your current operating system is ${coercedOSRelease.version}. Please try changing your MySQL version requirement, updating your OS to a newer version, or if you believe this is a bug, please report this on GitHub.` } - if (process.platform === 'linux' && etcOSRelease.NAME === 'Ubuntu' && etcOSRelease.VERSION_ID >= '24.04') { - //Since Ubuntu >= 24.04 uses libaio1t64 instead of libaio, this package has to copy libaio1t64 into a folder that MySQL looks in for dynamically linked libraries with the filename "libaio.so.1". - //I have not been able to find a suitable folder for libaio1t64 to be copied into for MySQL < 8.0.4, so here we are filtering all versions lower than 8.0.4 since they fail to launch in Ubuntu 24.04. - //If there is a suitable filepath for libaio1t64 to be copied into for MySQL < 8.0.4 then this check can be removed and these older MySQL versions can run on Ubuntu. - //Pull requests are welcome for adding >= Ubuntu 24.04 support for MySQL < 8.0.4. - //A way to get MySQL running on Ubuntu >= 24.04 is to symlink libaio1t64 to the location libaio would be. It is not suitable for this package to be doing that automatically, so instead this package has been copying libaio1t64 into the MySQL binary folder. - selectedVersions = selectedVersions.filter(v => !lt(v, '8.0.4')) - } - - if (selectedVersions.length === 0) { - throw `You are running a version of Ubuntu that is too modern to run any MySQL versions with this package that match the following version requirement: ${versionToGet}. Please choose a newer version of MySQL to use, or if you believe this is a bug please report this on GitHub.` + if (process.platform === 'linux') { + if (etcOSRelease.NAME === 'Ubuntu' && etcOSRelease.VERSION_ID >= '24.04') { + //Since Ubuntu >= 24.04 uses libaio1t64 instead of libaio, this package has to copy libaio1t64 into a folder that MySQL looks in for dynamically linked libraries with the filename "libaio.so.1". + //I have not been able to find a suitable folder for libaio1t64 to be copied into for MySQL < 8.0.4, so here we are filtering all versions lower than 8.0.4 since they fail to launch in Ubuntu 24.04. + //If there is a suitable filepath for libaio1t64 to be copied into for MySQL < 8.0.4 then this check can be removed and these older MySQL versions can run on Ubuntu. + //Pull requests are welcome for adding >= Ubuntu 24.04 support for MySQL < 8.0.4. + //A way to get MySQL running on Ubuntu >= 24.04 is to symlink libaio1t64 to the location libaio would be. It is not suitable for this package to be doing that automatically, so instead this package has been copying libaio1t64 into the MySQL binary folder. + selectedVersions = selectedVersions.filter(v => !lt(v, '8.0.4')) + + if (selectedVersions.length === 0) { + throw `You are running a version of Ubuntu that is too modern to run any MySQL versions with this package that match the following version requirement: ${versionToGet}. Please choose a newer version of MySQL to use, or if you believe this is a bug please report this on GitHub.` + } + } else if (isOnAlpineLinux) { + //https://github.com/Sebastian-Webster/mysql-server-musl-binaries only has support for v8.4.x and 9.x binaries + selectedVersions = selectedVersions.filter(v => satisfies(v, '8.4.x') || satisfies(v, '9.x')) + + if (selectedVersions.length === 0) { + throw 'mysql-memory-server has detected you are running this package on Alpine Linux. The source for MySQL with musl libc only provides binaries for MySQL 8.4.x and 9.x and as such only those versions can be used with this package. Please use 8.4.x or 9.x.' + } + } } //Sorts versions in descending order @@ -81,6 +90,7 @@ export default function getBinaryURL(versionToGet: string = "x", currentArch: st const isDMR = satisfies(selectedVersion, DMR_MYSQL_VERSIONS) let fileLocation: string = '' + let xPluginSupported = true; if (currentOS === 'win32') { fileLocation = `${major(selectedVersion)}.${minor(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-winx64.zip` @@ -88,6 +98,9 @@ export default function getBinaryURL(versionToGet: string = "x", currentArch: st const MySQLmacOSVersionNameKeys = Object.keys(MYSQL_MACOS_VERSIONS_IN_FILENAME); const macOSVersionNameKey = MySQLmacOSVersionNameKeys.find(range => satisfies(selectedVersion, range)) fileLocation = `${major(selectedVersion)}.${minor(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-${MYSQL_MACOS_VERSIONS_IN_FILENAME[macOSVersionNameKey]}-${currentArch === 'x64' ? 'x86_64' : 'arm64'}.tar.gz` + } else if (isOnAlpineLinux) { + fileLocation = `https://github.com/Sebastian-Webster/mysql-server-musl-binaries/releases/download/current/mysql-musl-${selectedVersion}-${currentArch === 'x64' ? 'x86_64' : 'arm64'}.tar.gz` + xPluginSupported = false } else if (currentOS === 'linux') { const glibcObject = MYSQL_LINUX_GLIBC_VERSIONS[currentArch]; const glibcVersionKeys = Object.keys(glibcObject); @@ -104,10 +117,14 @@ export default function getBinaryURL(versionToGet: string = "x", currentArch: st const fileExtension = MYSQL_LINUX_FILE_EXTENSIONS[currentArch][fileExtensionKey] fileLocation = `${major(selectedVersion)}.${minor(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-linux-${minimalInstallAvailable !== 'no-glibc-tag' ? `glibc${glibcVersion}-` : ''}${currentArch === 'x64' ? 'x86_64' : 'aarch64'}${minimalInstallAvailable !== 'no' && (process.arch !== 'arm64' ? true : satisfies(selectedVersion, MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE_ARM64)) ? `-minimal${satisfies(selectedVersion, MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS) ? '-rebuild' : ''}` : ''}.tar.${fileExtension}` + } else { + throw 'You are running this package on an unsupported OS. Please use either Windows, macOS, or a Linux-based OS.' } return { version: selectedVersion, - url: archiveBaseURL + fileLocation + url: fileLocation, + hostedByOracle: !isOnAlpineLinux, // Only the Alpine Linux binaries are not hosted on the MySQL CDN. + xPluginSupported } } \ No newline at end of file diff --git a/tests/versions.test.ts b/tests/versions.test.ts index 59a1033..c3fd8e1 100644 --- a/tests/versions.test.ts +++ b/tests/versions.test.ts @@ -28,7 +28,8 @@ for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, proce username: username, logLevel: 'LOG', initSQLString: 'CREATE DATABASE mytestdb;', - arch + arch, + xEnabled: process.env.X_OFF === 'true' ? 'OFF' : 'FORCE' } const db = await createDB(options) diff --git a/types/index.ts b/types/index.ts index 0f0979d..2bf8f74 100644 --- a/types/index.ts +++ b/types/index.ts @@ -55,8 +55,7 @@ export type MySQLDB = { username: string, mysql: { version: string, - versionIsInstalledOnSystem: boolean, - xPluginIsEnabled: boolean + versionIsInstalledOnSystem: boolean }, stop: () => Promise } @@ -64,12 +63,15 @@ export type MySQLDB = { export type DownloadedMySQLVersion = { version: string, path: string, - installedOnSystem: boolean + installedOnSystem: boolean, + xPluginSupported: boolean } export type BinaryInfo = { url: string, - version: string + version: string, + hostedByOracle: boolean, + xPluginSupported: boolean } export type OptionTypeChecks = {