diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 869a77f..c374968 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,26 +1,14 @@ name: CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: npm ci - - run: npm test - - name: clang-format (diff) - if: github.event_name == 'pull_request' - env: - BASE: ${{ github['base_ref'] }} - run: | - git fetch --no-tags --prune --depth=1 origin +refs/heads/$BASE:refs/remotes/origin/$BASE - npm run git-clang-format -- --diff origin/$BASE |\ - awk -v s="^diff" '$0~s{r=1} 1; END{exit(r)}' - - name: clang-format (project) - if: github.event_name == 'push' - run: | - npm run git-clang-format -- --diff --commit $(git hash-object -t tree /dev/null) |\ - awk -v s="^diff" '$0~s{r=1} 1; END{exit(r)}' + - uses: actions/checkout@v4 + - run: npm ci + - run: npm test + - run: npx prettier . --check diff --git a/.gitignore b/.gitignore index 76175dc..72aae85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules/ out/ - diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1fcb152 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +out diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7f5dba4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "bracketSpacing": false, + "singleQuote": true +} diff --git a/README.md b/README.md index c4ad044..ed3cd14 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # node-clangd + Shared features of vscode-clangd and coc-clangd diff --git a/package-lock.json b/package-lock.json index 0a5418f..27b42bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@clangd/install", - "version": "0.1.16", + "version": "0.1.18", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@clangd/install", - "version": "0.1.16", + "version": "0.1.18", "license": "Apache-2.0 WITH LLVM-exception", "dependencies": { "abort-controller": "^3.0.0", @@ -27,8 +27,8 @@ "@types/tape": "^4.13.0", "@types/tmp": "^0.2.0", "@types/which": "^1.3.2", - "clang-format": "^1.4.0", "node-static": "^0.7.11", + "prettier": "^3.3.3", "source-map-support": "^0.5.19", "tap-spec": "^5.0.0", "tape": "^5.0.0", @@ -183,12 +183,6 @@ "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", "dev": true }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -249,22 +243,6 @@ "node": ">=0.10.0" } }, - "node_modules/clang-format": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.4.0.tgz", - "integrity": "sha512-NrdyUnHJOGvMa60vbWk7GJTvOdhibj3uK5C0FlwdNG4301OUvqEJTFce9I9x8qw2odBbIVrJ+9xbsFS3a4FbDA==", - "dev": true, - "dependencies": { - "async": "^1.5.2", - "glob": "^7.0.0", - "resolve": "^1.1.6" - }, - "bin": { - "check-clang-format": "bin/check-clang-format.js", - "clang-format": "index.js", - "git-clang-format": "bin/git-clang-format" - } - }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -902,6 +880,22 @@ "node": ">=0.10.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", @@ -1557,12 +1551,6 @@ "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", "dev": true }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1617,17 +1605,6 @@ "supports-color": "^2.0.0" } }, - "clang-format": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.4.0.tgz", - "integrity": "sha512-NrdyUnHJOGvMa60vbWk7GJTvOdhibj3uK5C0FlwdNG4301OUvqEJTFce9I9x8qw2odBbIVrJ+9xbsFS3a4FbDA==", - "dev": true, - "requires": { - "async": "^1.5.2", - "glob": "^7.0.0", - "resolve": "^1.1.6" - } - }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -2133,6 +2110,12 @@ "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=", "dev": true }, + "prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true + }, "pretty-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", diff --git a/package.json b/package.json index 8d67424..873c3b6 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "scripts": { "compile": "tsc -watch -p ./", "test": "tsc -p ./ && tape -r source-map-support/register 'out/test/**/*.js' | tap-spec", - "format": "clang-format -i --glob=\"{src,test}/*.ts\"", - "git-clang-format": "git-clang-format", + "format": "prettier . --write", "prepare": "tsc -p ./" }, "repository": { @@ -45,8 +44,8 @@ "@types/tape": "^4.13.0", "@types/tmp": "^0.2.0", "@types/which": "^1.3.2", - "clang-format": "^1.4.0", "node-static": "^0.7.11", + "prettier": "^3.3.3", "source-map-support": "^0.5.19", "tap-spec": "^5.0.0", "tape": "^5.0.0", diff --git a/src/index.ts b/src/index.ts index 455b3b4..43e50bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,14 +46,16 @@ type UI = { promptInstall(version: string): void; // Ask whether to reuse rather than overwrite an existing clangd installation. // Undefined means no choice was made, so we shouldn't do either. - shouldReuse(path: string): Promise; + shouldReuse(path: string): Promise; // `work` may take a while to resolve, indicate we're doing something. slow(title: string, work: Promise): Promise; // `work` will take a while to run and can indicate fractional progress. - progress(title: string, cancel: AbortController|null, - work: (progress: (fraction: number) => void) => Promise): - Promise; + progress( + title: string, + cancel: AbortController | null, + work: (progress: (fraction: number) => void) => Promise, + ): Promise; /** * Get localization string. @@ -66,26 +68,28 @@ type UI = { * @returns localized string with injected arguments. * @example `localize('Hello {0}!', 'World');` */ - localize(message: string, ...args: Array): string; -} + localize(message: string, ...args: Array): string; +}; type InstallStatus = { // Absolute path to clangd, or null if no valid clangd binary is configured. - clangdPath: string|null; + clangdPath: string | null; // Background tasks that were started, exposed for testing. background: Promise; }; // Main startup workflow: check whether the configured clangd binary us usable. // If not, offer to install one. If so, check for updates. -export async function prepare(ui: UI, - checkUpdate: boolean): Promise { +export async function prepare( + ui: UI, + checkUpdate: boolean, +): Promise { let clangdPath = ui.clangdPath; try { if (path.isAbsolute(clangdPath)) { await promisify(fs.access)(clangdPath); } else { - clangdPath = await promisify(which)(clangdPath) as string; + clangdPath = (await promisify(which)(clangdPath)) as string; } } catch (e) { // Couldn't find clangd - start recovery flow and stop extension loading. @@ -94,8 +98,9 @@ export async function prepare(ui: UI, // Allow extension to load, asynchronously check for updates. return { clangdPath, - background: checkUpdate ? checkUpdates(/*requested=*/ false, ui) - : Promise.resolve() + background: checkUpdate + ? checkUpdates(/*requested=*/ false, ui) + : Promise.resolve(), }; } @@ -112,8 +117,9 @@ export async function installLatest(ui: UI) { if (!abort.signal.aborted) { console.error('Failed to install clangd: ', e); const message = ui.localize( - 'Failed to install clangd language server: {0}\nYou may want to install it manually.', - e) + 'Failed to install clangd language server: {0}\nYou may want to install it manually.', + e, + ); ui.showHelp(message, installURL); } } @@ -133,13 +139,22 @@ export async function checkUpdates(requested: boolean, ui: UI) { ui.error(ui.localize('Failed to check for clangd update: {0}', e)); return; } - console.log('Checking for clangd update: available=', upgrade.new, - ' installed=', upgrade.old); + console.log( + 'Checking for clangd update: available=', + upgrade.new, + ' installed=', + upgrade.old, + ); // Bail out if the new version is better or comparable. if (!upgrade.upgrade) { if (requested) - ui.info(ui.localize('clangd is up-to-date (you have {0}, latest is {1})', - upgrade.old, upgrade.new)); + ui.info( + ui.localize( + 'clangd is up-to-date (you have {0}, latest is {1})', + upgrade.old, + upgrade.new, + ), + ); return; } ui.promptUpdate(upgrade.old, upgrade.new); @@ -155,79 +170,98 @@ async function recover(ui: UI) { ui.promptInstall(release.name); } catch (e) { console.error('Auto-install failed: ', e); - ui.showHelp(ui.localize('The clangd language server is not installed.'), - installURL); + ui.showHelp( + ui.localize('The clangd language server is not installed.'), + installURL, + ); } } const installURL = 'https://clangd.llvm.org/installation.html'; // The GitHub API endpoint for the latest binary clangd release. let githubReleaseURL = - 'https://api.github.com/repos/clangd/clangd/releases/latest'; + 'https://api.github.com/repos/clangd/clangd/releases/latest'; // Set a fake URL for testing. -export function fakeGitHubReleaseURL(u: string) { githubReleaseURL = u; } +export function fakeGitHubReleaseURL(u: string) { + githubReleaseURL = u; +} let lddCommand = 'ldd'; -export function fakeLddCommand(l: string) { lddCommand = l; } +export function fakeLddCommand(l: string) { + lddCommand = l; +} // Bits for talking to github's release API namespace Github { -export interface Release { - name: string, tag_name: string, assets: Array, -} -export interface Asset { - name: string, browser_download_url: string, -} + export interface Release { + name: string; + tag_name: string; + assets: Array; + } + export interface Asset { + name: string; + browser_download_url: string; + } -// Fetch the metadata for the latest stable clangd release. -export async function latestRelease(): Promise { - const timeoutController = new AbortController(); - const timeout = setTimeout(() => { timeoutController.abort(); }, 5000); - try { - const response = - await fetch(githubReleaseURL, {signal: timeoutController.signal}); - if (!response.ok) { - console.log(response.url, response.status, response.statusText); - throw new Error(`Can't fetch release: ${response.statusText}`); + // Fetch the metadata for the latest stable clangd release. + export async function latestRelease(): Promise { + const timeoutController = new AbortController(); + const timeout = setTimeout(() => { + timeoutController.abort(); + }, 5000); + try { + const response = await fetch(githubReleaseURL, { + signal: timeoutController.signal, + }); + if (!response.ok) { + console.log(response.url, response.status, response.statusText); + throw new Error(`Can't fetch release: ${response.statusText}`); + } + return (await response.json()) as Release; + } finally { + clearTimeout(timeout); } - return await response.json() as Release; - } finally { - clearTimeout(timeout); } -} -// Determine which release asset should be installed for this machine. -export async function chooseAsset(release: Github.Release): - Promise { - const variants: {[key: string]: string} = { - 'win32': 'windows', - 'linux': 'linux', - 'darwin': 'mac', - }; - const variant = variants[os.platform()]; - if (variant == 'linux') { - // Hardcoding this here is sad, but we'd like to offer a nice error message - // without making the user download the package first. - const minGlibc = new semver.Range('2.18'); - const oldGlibc = await Version.oldGlibc(minGlibc); - if (oldGlibc) { - throw new Error('The clangd release is not compatible with your system ' + - `(glibc ${oldGlibc.raw} < ${minGlibc.raw}). ` + - 'Try to install it using your package manager instead.'); + // Determine which release asset should be installed for this machine. + export async function chooseAsset( + release: Github.Release, + ): Promise { + const variants: {[key: string]: string} = { + win32: 'windows', + linux: 'linux', + darwin: 'mac', + }; + const variant = variants[os.platform()]; + if (variant == 'linux') { + // Hardcoding this here is sad, but we'd like to offer a nice error message + // without making the user download the package first. + const minGlibc = new semver.Range('2.18'); + const oldGlibc = await Version.oldGlibc(minGlibc); + if (oldGlibc) { + throw new Error( + 'The clangd release is not compatible with your system ' + + `(glibc ${oldGlibc.raw} < ${minGlibc.raw}). ` + + 'Try to install it using your package manager instead.', + ); + } } + // 32-bit vscode is still common on 64-bit windows, so don't reject that. + if ( + variant && + (os.arch() == 'x64' || + variant == 'windows' || + // Mac distribution contains a fat binary working on both x64 + // and arm64s. + (os.arch() == 'arm64' && variant == 'mac')) + ) { + const substr = 'clangd-' + variant; + const asset = release.assets.find((a) => a.name.indexOf(substr) >= 0); + if (asset) return asset; + } + throw new Error( + `No clangd ${release.name} binary available for ${os.platform()}/${os.arch()}`, + ); } - // 32-bit vscode is still common on 64-bit windows, so don't reject that. - if (variant && (os.arch() == 'x64' || variant == 'windows' || - // Mac distribution contains a fat binary working on both x64 - // and arm64s. - (os.arch() == 'arm64' && variant == 'mac'))) { - const substr = 'clangd-' + variant; - const asset = release.assets.find(a => a.name.indexOf(substr) >= 0); - if (asset) - return asset; - } - throw new Error(`No clangd ${release.name} binary available for ${ - os.platform()}/${os.arch()}`); -} } // Functions to download and install the releases, and manage the files on disk. @@ -242,75 +276,89 @@ export async function chooseAsset(release: Github.Release): // download/ // clangd-platform-.zip (deleted after extraction) namespace Install { -// Download the binary archive `asset` from a github `release` and extract it -// to the extension's global storage location. -// The `abort` controller is signaled if the user cancels the installation. -// Returns the absolute path to the installed clangd executable. -export async function install(release: Github.Release, asset: Github.Asset, - abort: AbortController, ui: UI): Promise { - const dirs = await createDirs(ui); - const extractRoot = path.join(dirs.install, release.tag_name); - if (await promisify(fs.exists)(extractRoot)) { - const reuse = await ui.shouldReuse(release.name); - if (reuse === undefined) { - // User dismissed prompt, bail out. - abort.abort(); - throw new Error(`clangd ${release.name} already installed!`); - } - if (reuse) { - // Find clangd within the existing directory. - let files = (await readdirp.promise(extractRoot)).map(e => e.fullPath); - return findExecutable(files); - } else { - // Delete the old version. - await promisify(rimraf)(extractRoot); - // continue with installation. + // Download the binary archive `asset` from a github `release` and extract it + // to the extension's global storage location. + // The `abort` controller is signaled if the user cancels the installation. + // Returns the absolute path to the installed clangd executable. + export async function install( + release: Github.Release, + asset: Github.Asset, + abort: AbortController, + ui: UI, + ): Promise { + const dirs = await createDirs(ui); + const extractRoot = path.join(dirs.install, release.tag_name); + if (await promisify(fs.exists)(extractRoot)) { + const reuse = await ui.shouldReuse(release.name); + if (reuse === undefined) { + // User dismissed prompt, bail out. + abort.abort(); + throw new Error(`clangd ${release.name} already installed!`); + } + if (reuse) { + // Find clangd within the existing directory. + let files = (await readdirp.promise(extractRoot)).map( + (e) => e.fullPath, + ); + return findExecutable(files); + } else { + // Delete the old version. + await promisify(rimraf)(extractRoot); + // continue with installation. + } } + const zipFile = path.join(dirs.download, asset.name); + await download(asset.browser_download_url, zipFile, abort, ui); + const zip = new AdmZip(zipFile); + await ui.slow( + ui.localize('Extracting {0}', asset.name), + new Promise((resolve) => { + zip.extractAllToAsync(extractRoot, true, false, resolve); + }), + ); + const executable = findExecutable(zip.getEntries().map((e) => e.entryName)); + const clangdPath = path.join(extractRoot, executable); + await fs.promises.chmod(clangdPath, 0o755); + await fs.promises.unlink(zipFile); + return clangdPath; } - const zipFile = path.join(dirs.download, asset.name); - await download(asset.browser_download_url, zipFile, abort, ui); - const zip = new AdmZip(zipFile); - await ui.slow(ui.localize('Extracting {0}', asset.name), - new Promise(resolve => { - zip.extractAllToAsync(extractRoot, true, false, resolve); - })); - const executable = findExecutable(zip.getEntries().map(e => e.entryName)); - const clangdPath = path.join(extractRoot, executable); - await fs.promises.chmod(clangdPath, 0o755); - await fs.promises.unlink(zipFile); - return clangdPath; -} -// Create the 'install' and 'download' directories, and return absolute paths. -async function createDirs(ui: UI) { - const install = path.join(ui.storagePath, 'install'); - const download = path.join(ui.storagePath, 'download'); - for (const dir of [install, download]) - await fs.promises.mkdir(dir, {'recursive': true}); - return {install: install, download: download}; -} + // Create the 'install' and 'download' directories, and return absolute paths. + async function createDirs(ui: UI) { + const install = path.join(ui.storagePath, 'install'); + const download = path.join(ui.storagePath, 'download'); + for (const dir of [install, download]) + await fs.promises.mkdir(dir, {recursive: true}); + return {install: install, download: download}; + } -// Find the clangd executable within a set of files. -function findExecutable(paths: string[]): string { - const filename = os.platform() == 'win32' ? 'clangd.exe' : 'clangd'; - const entry = paths.find(f => path.posix.basename(f) == filename || - path.win32.basename(f) == filename); - if (entry == null) - throw new Error('Didn\'t find a clangd executable!'); - return entry; -} + // Find the clangd executable within a set of files. + function findExecutable(paths: string[]): string { + const filename = os.platform() == 'win32' ? 'clangd.exe' : 'clangd'; + const entry = paths.find( + (f) => + path.posix.basename(f) == filename || + path.win32.basename(f) == filename, + ); + if (entry == null) throw new Error("Didn't find a clangd executable!"); + return entry; + } -// Downloads `url` to a local file `dest` (whose parent should exist). -// A progress dialog is shown, if it is cancelled then `abort` is signaled. -async function download(url: string, dest: string, abort: AbortController, - ui: UI): Promise { - console.log('Downloading ', url, ' to ', dest); - return ui.progress( - ui.localize('Downloading {0}', path.basename(dest)), abort, + // Downloads `url` to a local file `dest` (whose parent should exist). + // A progress dialog is shown, if it is cancelled then `abort` is signaled. + async function download( + url: string, + dest: string, + abort: AbortController, + ui: UI, + ): Promise { + console.log('Downloading ', url, ' to ', dest); + return ui.progress( + ui.localize('Downloading {0}', path.basename(dest)), + abort, async (progress) => { const response = await fetch(url, {signal: abort.signal}); - if (!response.ok) - throw new Error(`Failed to download ${url}`); + if (!response.ok) throw new Error(`Failed to download ${url}`); const size = Number(response.headers.get('content-length')); let read = 0; response.body.on('data', (chunk: Buffer) => { @@ -318,13 +366,14 @@ async function download(url: string, dest: string, abort: AbortController, progress(read / size); }); const out = fs.createWriteStream(dest); - await promisify(stream.pipeline)(response.body, out).catch(e => { + await promisify(stream.pipeline)(response.body, out).catch((e) => { // Clean up the partial file if the download failed. fs.unlink(dest, (_) => null); // Don't wait, and ignore error. throw e; }); - }); -} + }, + ); + } } // Functions dealing with clangd versions. @@ -336,76 +385,78 @@ async function download(url: string, dest: string, abort: AbortController, // These functions throw if versions can't be parsed (e.g. installed clangd // is a vendor-modified version). namespace Version { -export async function upgrade(release: Github.Release, clangdPath: string) { - const releasedVer = released(release); - const installedVer = await installed(clangdPath); - return { - old: installedVer.raw, - new: releasedVer.raw, - upgrade: rangeGreater(releasedVer, installedVer) - }; -} + export async function upgrade(release: Github.Release, clangdPath: string) { + const releasedVer = released(release); + const installedVer = await installed(clangdPath); + return { + old: installedVer.raw, + new: releasedVer.raw, + upgrade: rangeGreater(releasedVer, installedVer), + }; + } -const loose: semver.Options = { - 'loose': true -}; + const loose: semver.Options = { + loose: true, + }; -// Get the version of an installed clangd binary using `clangd --version`. -async function installed(clangdPath: string): Promise { - const output = await run(clangdPath, ['--version']); - console.log(clangdPath, ' --version output: ', output); - const prefix = 'clangd version '; - const pos = output.indexOf(prefix); - if (pos < 0) - throw new Error(`Couldn't parse clangd --version output: ${output}`); - if (pos > 0) { - const vendor = output.substring(0, pos).trim(); - if (vendor == 'Apple') - throw new Error(`Cannot compare vendor's clangd version: ${output}`); + // Get the version of an installed clangd binary using `clangd --version`. + async function installed(clangdPath: string): Promise { + const output = await run(clangdPath, ['--version']); + console.log(clangdPath, ' --version output: ', output); + const prefix = 'clangd version '; + const pos = output.indexOf(prefix); + if (pos < 0) + throw new Error(`Couldn't parse clangd --version output: ${output}`); + if (pos > 0) { + const vendor = output.substring(0, pos).trim(); + if (vendor == 'Apple') + throw new Error(`Cannot compare vendor's clangd version: ${output}`); + } + // Some vendors add trailing ~patchlevel, ignore this. + const rawVersion = output.substr(pos + prefix.length).split(/\s|~/, 1)[0]; + return new semver.Range(rawVersion, loose); } - // Some vendors add trailing ~patchlevel, ignore this. - const rawVersion = output.substr(pos + prefix.length).split(/\s|~/, 1)[0]; - return new semver.Range(rawVersion, loose); -} -// Get the version of a github release, by parsing the tag or name. -function released(release: Github.Release): semver.Range { - // Prefer the tag name, but fall back to the release name. - return (!semver.validRange(release.tag_name, loose) && - semver.validRange(release.name, loose)) - ? new semver.Range(release.name, loose) - : new semver.Range(release.tag_name, loose); -} + // Get the version of a github release, by parsing the tag or name. + function released(release: Github.Release): semver.Range { + // Prefer the tag name, but fall back to the release name. + return !semver.validRange(release.tag_name, loose) && + semver.validRange(release.name, loose) + ? new semver.Range(release.name, loose) + : new semver.Range(release.tag_name, loose); + } -// Detect the (linux) system's glibc version. If older than `min`, return it. -export async function oldGlibc(min: semver.Range): Promise { - // ldd is distributed with glibc, so ldd --version should be a good proxy. - const output = await run(lddCommand, ['--version']); - // The first line is e.g. "ldd (Debian GLIBC 2.29-9) 2.29". - const line = output.split('\n', 1)[0]; - // Require some confirmation this is [e]glibc, and a plausible - // version number. - const match = line.match(/^ldd .*glibc.* (\d+(?:\.\d+)+)[^ ]*$/i); - if (!match || !semver.validRange(match[1], loose)) { - console.error(`Can't glibc version from ldd --version output: ${line}`); - return null; + // Detect the (linux) system's glibc version. If older than `min`, return it. + export async function oldGlibc( + min: semver.Range, + ): Promise { + // ldd is distributed with glibc, so ldd --version should be a good proxy. + const output = await run(lddCommand, ['--version']); + // The first line is e.g. "ldd (Debian GLIBC 2.29-9) 2.29". + const line = output.split('\n', 1)[0]; + // Require some confirmation this is [e]glibc, and a plausible + // version number. + const match = line.match(/^ldd .*glibc.* (\d+(?:\.\d+)+)[^ ]*$/i); + if (!match || !semver.validRange(match[1], loose)) { + console.error(`Can't glibc version from ldd --version output: ${line}`); + return null; + } + const version = new semver.Range(match[1], loose); + console.log('glibc is', version.raw, 'min is', min.raw); + return rangeGreater(min, version) ? version : null; } - const version = new semver.Range(match[1], loose); - console.log('glibc is', version.raw, 'min is', min.raw); - return rangeGreater(min, version) ? version : null; -} -// Run a system command and capture any stdout produced. -async function run(command: string, flags: string[]): Promise { - const child = child_process.spawn(command, flags, - {stdio: ['ignore', 'pipe', 'ignore']}); - let output = ''; - for await (const chunk of child.stdout) - output += chunk; - return output; -} + // Run a system command and capture any stdout produced. + async function run(command: string, flags: string[]): Promise { + const child = child_process.spawn(command, flags, { + stdio: ['ignore', 'pipe', 'ignore'], + }); + let output = ''; + for await (const chunk of child.stdout) output += chunk; + return output; + } -function rangeGreater(newVer: semver.Range, oldVer: semver.Range) { - return semver.gtr(semver.minVersion(newVer), oldVer); -} + function rangeGreater(newVer: semver.Range, oldVer: semver.Range) { + return semver.gtr(semver.minVersion(newVer), oldVer); + } } diff --git a/test/index.ts b/test/index.ts index 29eb67d..cf520f1 100644 --- a/test/index.ts +++ b/test/index.ts @@ -12,7 +12,7 @@ const oldClangd = process.cwd() + '/test/assets/fake-clangd-5/clangd'; const newClangdV15 = process.cwd() + '/test/assets/fake-clangd-15/clangd'; const newClangdV16 = process.cwd() + '/test/assets/fake-clangd-16/clangd'; const unversionedClangd = - process.cwd() + '/test/assets/fake-clangd-unversioned/clangd'; + process.cwd() + '/test/assets/fake-clangd-unversioned/clangd'; const appleClangd = process.cwd() + '/test/assets/apple-clangd-5/clangd'; const exactLdd = process.cwd() + '/test/assets/ldd/exact'; const oldLdd = process.cwd() + '/test/assets/ldd/old'; @@ -49,9 +49,15 @@ class FakeUI { console.info(msg, url); } - promptReload() { this.event('promptReload'); } - promptUpdate() { this.event('promptUpdate'); } - promptInstall() { this.event('promptInstall'); } + promptReload() { + this.event('promptReload'); + } + promptUpdate() { + this.event('promptUpdate'); + } + promptInstall() { + this.event('promptInstall'); + } public shouldReuseValue = true; async shouldReuse() { this.event('shouldReuse'); @@ -62,45 +68,56 @@ class FakeUI { this.event('slow'); return work; } - progress(_title: string, _cancel: any, - work: (progress: (fraction: number) => void) => Promise) { + progress( + _title: string, + _cancel: any, + work: (progress: (fraction: number) => void) => Promise, + ) { this.event('progress'); return work((fraction) => console.log('progress% ', 100 * fraction)); } - localize(message: string, ...args: Array): string { + localize(message: string, ...args: Array): string { let ret = message; for (const i in args) { ret.replace(`{${i}}`, args[i].toString()); } return ret; } -}; - -function test(name: string, - body: (assert: tape.Test, ui: FakeUI) => Promise) { - tape(name, async (assert) => tmp.withDir(async dir => { - const ui = new FakeUI(dir.path); - const files = new nodeStatic.Server('test/assets/'); - return new Promise((resolve, _reject) => { - const server = http.createServer((req, res) => { - console.log('Fake github:', req.method, req.url); - req.on('end', () => files.serve(req, res)).resume(); - }) - .listen(9999, '::', async () => { - console.log('Fake github serving...'); - install.fakeGitHubReleaseURL(releases); - install.fakeLddCommand(exactLdd); - try { - await body(assert, ui); - } catch (e) { - assert.fail(e); - } - console.log('Fake github stopping...'); - server.close(); - resolve(); - }); - }); - }, {unsafeCleanup: true})); +} + +function test( + name: string, + body: (assert: tape.Test, ui: FakeUI) => Promise, +) { + tape(name, async (assert) => + tmp.withDir( + async (dir) => { + const ui = new FakeUI(dir.path); + const files = new nodeStatic.Server('test/assets/'); + return new Promise((resolve, _reject) => { + const server = http + .createServer((req, res) => { + console.log('Fake github:', req.method, req.url); + req.on('end', () => files.serve(req, res)).resume(); + }) + .listen(9999, '::', async () => { + console.log('Fake github serving...'); + install.fakeGitHubReleaseURL(releases); + install.fakeLddCommand(exactLdd); + try { + await body(assert, ui); + } catch (e) { + assert.fail(e); + } + console.log('Fake github stopping...'); + server.close(); + resolve(); + }); + }); + }, + {unsafeCleanup: true}, + ), + ); } // Test the actual installation, typically the clangd.install command. @@ -108,25 +125,44 @@ function test(name: string, test('install', async (assert, ui) => { await install.installLatest(ui); - const installedClangd = - path.join(ui.storagePath, 'install', '10.0', 'fake-clangd-10', 'clangd'); - assert.true(fs.existsSync(installedClangd), - `Extracted clangd exists: ${installedClangd}`); + const installedClangd = path.join( + ui.storagePath, + 'install', + '10.0', + 'fake-clangd-10', + 'clangd', + ); + assert.true( + fs.existsSync(installedClangd), + `Extracted clangd exists: ${installedClangd}`, + ); assert.equal(ui.clangdPath, installedClangd); - assert.deepEqual( - ui.events, [/*download*/ 'progress', /*extract*/ 'slow', 'promptReload']); + assert.deepEqual(ui.events, [ + /*download*/ 'progress', + /*extract*/ 'slow', + 'promptReload', + ]); }); test('install: no binary for platform', async (assert, ui) => { install.fakeGitHubReleaseURL(incompatibleReleases); await install.installLatest(ui); - const installedClangd = - path.join(ui.storagePath, 'install', '10.0', 'fake-clangd-10', 'clangd'); - assert.false(fs.existsSync(installedClangd), - `Extracted clangd exists: ${installedClangd}`); - assert.true(ui.clangdPath.endsWith('fake-clangd-5/clangd'), - 'clangdPath unmodified'); + const installedClangd = path.join( + ui.storagePath, + 'install', + '10.0', + 'fake-clangd-10', + 'clangd', + ); + assert.false( + fs.existsSync(installedClangd), + `Extracted clangd exists: ${installedClangd}`, + ); + assert.true( + ui.clangdPath.endsWith('fake-clangd-5/clangd'), + 'clangdPath unmodified', + ); assert.deepEqual(ui.events, ['showHelp']); }); @@ -134,14 +170,25 @@ test('install: wrong url', async (assert, ui) => { install.fakeGitHubReleaseURL(wrongUrlReleases); await install.installLatest(ui); - const installedClangd = - path.join(ui.storagePath, 'install', '10.0', 'fake-clangd-10', 'clangd'); - assert.false(fs.existsSync(installedClangd), - `Extracted clangd exists: ${installedClangd}`); - assert.true(ui.clangdPath.endsWith('fake-clangd-5/clangd'), - 'clangdPath unmodified'); - assert.deepEqual(ui.events, - [/*download*/ 'progress', /*download-fails*/ 'showHelp']); + const installedClangd = path.join( + ui.storagePath, + 'install', + '10.0', + 'fake-clangd-10', + 'clangd', + ); + assert.false( + fs.existsSync(installedClangd), + `Extracted clangd exists: ${installedClangd}`, + ); + assert.true( + ui.clangdPath.endsWith('fake-clangd-5/clangd'), + 'clangdPath unmodified', + ); + assert.deepEqual(ui.events, [ + /*download*/ 'progress', + /*download-fails*/ 'showHelp', + ]); }); if (os.platform() == 'linux') { @@ -177,8 +224,13 @@ test('install: reuse existing install', async (assert, ui) => { ui.shouldReuseValue = true; await install.installLatest(ui); - const installedClangd = - path.join(ui.storagePath, 'install', '10.0', 'fake-clangd-10', 'clangd'); + const installedClangd = path.join( + ui.storagePath, + 'install', + '10.0', + 'fake-clangd-10', + 'clangd', + ); assert.false(fs.existsSync(installedClangd), 'Not extracted'); assert.true(fs.existsSync(existingClangd), 'Not erased'); assert.equal(existingClangd, ui.clangdPath, 'clangdPath is existing install'); @@ -194,13 +246,21 @@ test('install: overwrite existing install', async (assert, ui) => { ui.shouldReuseValue = false; await install.installLatest(ui); - const installedClangd = - path.join(ui.storagePath, 'install', '10.0', 'fake-clangd-10', 'clangd'); + const installedClangd = path.join( + ui.storagePath, + 'install', + '10.0', + 'fake-clangd-10', + 'clangd', + ); assert.true(fs.existsSync(installedClangd), 'Extracted'); assert.false(fs.existsSync(existingClangd), 'Erased'); assert.equal(installedClangd, ui.clangdPath, 'clangdPath is new install'); assert.deepEqual(ui.events, [ - 'shouldReuse', /*download*/ 'progress', /*extract*/ 'slow', 'promptReload' + 'shouldReuse', + /*download*/ 'progress', + /*extract*/ 'slow', + 'promptReload', ]); }); diff --git a/tsconfig.json b/tsconfig.json index 929e475..1a1dd34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,13 @@ "target": "es6", "outDir": "out", "lib": [ - "es6", "es2015.core", "es2015.collection", "es2015.generator", - "es2015.iterable", "es2015.promise", "es2015.symbol" + "es6", + "es2015.core", + "es2015.collection", + "es2015.generator", + "es2015.iterable", + "es2015.promise", + "es2015.symbol" ], "sourceMap": true, "declaration": true,