diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index a735698e..8b86716b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -4,7 +4,7 @@ jobs: tests: strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest] gcc_v: [11] node-version: [18.x] fail-fast: false @@ -33,6 +33,16 @@ jobs: --slave /usr/bin/g++ g++ /usr/bin/g++-${GCC_V} \ --slave /usr/bin/gcov gcov /usr/bin/gcov-${GCC_V} + - name: Install GCC compilers Windows + if: contains(matrix.os, 'windows') + run: | + Invoke-WebRequest -Uri $Env:GCC_DOWNLOAD -OutFile mingw-w64.zip + Expand-Archive mingw-w64.zip + echo "$pwd\mingw-w64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + env: + GCC_DOWNLOAD: | + https://github.com/brechtsanders/winlibs_mingw/releases/download/13.0.1-snapshot20230122-10.0.0-msvcrt-r1/winlibs-i686-posix-dwarf-gcc-13.0.1-snapshot20230122-mingw-w64msvcrt-10.0.0-r1.zip + - name: Installing Extension run: npm ci - name: Compile @@ -42,12 +52,12 @@ jobs: - name: Test Syntax Highlighting run: npm run test:grammar - name: Test Unittests - uses: GabrielBB/xvfb-action@v1 + uses: GabrielBB/xvfb-action@v1.6 with: run: npm run test # This will not fail the job if tests fail so we have to npm test separately - name: Coverage report - uses: GabrielBB/xvfb-action@v1 + uses: GabrielBB/xvfb-action@v1.6 with: run: npm run coverage - name: Upload coverage to Codecov diff --git a/package-lock.json b/package-lock.json index bee14919..fbf88941 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@typescript-eslint/parser": "^5.62.0", "@vscode/test-electron": "^2.3.4", "c8": "^8.0.1", + "copy-webpack-plugin": "^11.0.0", "eslint": "^8.51.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsdoc": "^46.8.2", @@ -1230,6 +1231,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -2289,6 +2329,114 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6419,6 +6567,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", @@ -9349,6 +9506,35 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -10102,6 +10288,80 @@ } } }, + "copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "requires": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + } + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -13082,6 +13342,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", diff --git a/package.json b/package.json index 5491126f..c988aace 100644 --- a/package.json +++ b/package.json @@ -394,7 +394,7 @@ "properties": { "fortran.fortls.path": { "type": "string", - "default": "fortls", + "default": "", "markdownDescription": "Path to the Fortran language server (`fortls`).", "order": 10 }, @@ -758,6 +758,7 @@ "@typescript-eslint/parser": "^5.62.0", "@vscode/test-electron": "^2.3.4", "c8": "^8.0.1", + "copy-webpack-plugin": "^11.0.0", "eslint": "^8.51.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsdoc": "^46.8.2", diff --git a/scripts/get_console_script.py b/scripts/get_console_script.py new file mode 100644 index 00000000..6bf19445 --- /dev/null +++ b/scripts/get_console_script.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sys +import pkg_resources + + +def get_console_script_path(package_name: str, script_name: str) -> str | None: + # """ + # Get the absolute path of a console script from a package. + + # @param package_name: The name of the package. + # @param script_name: The name of the console script. + # @return The absolute path of the console script. + # """ + try: + # Get the distribution object for the package + dist = pkg_resources.get_distribution(package_name) + # Get the entry point for the console script + entry_point = dist.get_entry_info("console_scripts", script_name) + # Get the path to the script + script_path = entry_point.module_name.split(":")[0] + # Return the absolute path of the script + return pkg_resources.resource_filename(dist, script_path) + except Exception as e: + # Handle any exceptions that occur + print(f"Error: {e}") + return None + + +print(get_console_script_path(sys.argv[1], sys.argv[2])) diff --git a/scripts/get_pip_bin_dir.py b/scripts/get_pip_bin_dir.py new file mode 100644 index 00000000..4aec16ae --- /dev/null +++ b/scripts/get_pip_bin_dir.py @@ -0,0 +1,21 @@ +import os +import sysconfig + + +def get_install_bin_dir() -> str: + """Get the path to the pip install directory for scripts (bin, Scripts). + Works for virtualenv, conda, and system installs. + + Returns + ------- + str + Path to the pip install directory for scripts (bin, Scripts). + """ + if ("VIRTUAL_ENV" in os.environ) or ("CONDA_PREFIX" in os.environ): + return sysconfig.get_path("scripts") + else: + return sysconfig.get_path("scripts", f"{os.name}_user") + + +if __name__ == "__main__": + print(get_install_bin_dir()) diff --git a/scripts/mod_in_env.py b/scripts/mod_in_env.py new file mode 100644 index 00000000..a5bdb11c --- /dev/null +++ b/scripts/mod_in_env.py @@ -0,0 +1,8 @@ +#! /usr/bin/env python3 + +import sys +import pkg_resources + +# If not present, fails with a DistributionNotFound exception +if pkg_resources.get_distribution(str(sys.argv[1])).version: + exit(0) diff --git a/src/format/provider.ts b/src/format/provider.ts index 88d4c1ad..14ac8dfc 100644 --- a/src/format/provider.ts +++ b/src/format/provider.ts @@ -6,12 +6,12 @@ import * as vscode from 'vscode'; import which from 'which'; import { Logger } from '../services/logging'; +import { spawnAsPromise } from '../util/shell'; import { FORMATTERS, EXTENSION_ID, promptForMissingTool, getWholeFileRange, - spawnAsPromise, pathRelToAbs, } from '../util/tools'; diff --git a/src/lint/provider.ts b/src/lint/provider.ts index ef4a9083..947d7b63 100644 --- a/src/lint/provider.ts +++ b/src/lint/provider.ts @@ -21,14 +21,13 @@ import { import { Logger } from '../services/logging'; import { GlobPaths } from '../util/glob-paths'; import { arraysEqual } from '../util/helper'; +import { spawnAsPromise, shellTask } from '../util/shell'; import { EXTENSION_ID, resolveVariables, promptForMissingTool, isFreeForm, - spawnAsPromise, isFortran, - shellTask, } from '../util/tools'; import { GNULinter, GNUModernLinter, IntelLinter, LFortranLinter, NAGLinter } from './compilers'; diff --git a/src/lsp/client.ts b/src/lsp/client.ts index 7543cee8..4f4129d6 100644 --- a/src/lsp/client.ts +++ b/src/lsp/client.ts @@ -10,13 +10,13 @@ import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-lan import { RestartLS } from '../commands/commands'; import { Logger } from '../services/logging'; +import { Python } from '../util/python'; import { EXTENSION_ID, FortranDocumentSelector, LS_NAME, isFortran, getOuterMostWorkspaceFolder, - pipInstall, resolveVariables, } from '../util/tools'; @@ -26,8 +26,13 @@ import { export const clients: Map = new Map(); export class FortlsClient { + private readonly python: Python; + private readonly config: vscode.WorkspaceConfiguration; + constructor(private logger: Logger, private context?: vscode.ExtensionContext) { this.logger.debug('[lsp.client] Fortran Language Server -- constructor'); + this.python = new Python(); + this.config = workspace.getConfiguration(EXTENSION_ID); // if context is present if (context !== undefined) { @@ -313,6 +318,7 @@ export class FortlsClient { const ls = await this.fortlsPath(); // Check for version, if this fails fortls provided is invalid + const pipBin: string = await this.python.getPipBinDir(); const results = spawnSync(ls, ['--version']); const msg = `It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more. For a full list of features the language server adds see: https://fortls.fortran-lang.org`; @@ -323,7 +329,7 @@ export class FortlsClient { if (opt === 'Install') { try { this.logger.info(`[lsp.client] Downloading ${LS_NAME}`); - const msg = await pipInstall(LS_NAME); + const msg = await this.python.pipInstall(LS_NAME); window.showInformationMessage(msg); this.logger.info(`[lsp.client] ${LS_NAME} installed`); resolve(false); @@ -375,12 +381,21 @@ export class FortlsClient { const root = folder ? getOuterMostWorkspaceFolder(folder).uri : vscode.Uri.parse(os.homedir()); const config = workspace.getConfiguration(EXTENSION_ID); - let executablePath = resolveVariables(config.get('fortls.path')); + // TODO: make the default value undefined, make windows use fortls.exe + // get the full path of the Python bin dir, check if file exists + // else we are running a script with a relative path (verify this is the case) + let executablePath = config.get('fortls.path'); + if (!executablePath || this.isFortlsPathDefault(executablePath)) { + executablePath = 'fortls' + (os.platform() === 'win32' ? '.exe' : ''); + executablePath = path.join(await this.python.getPipBinDir(), executablePath); + } else { + executablePath = resolveVariables(executablePath); + } // The path can be resolved as a relative path if: // 1. it does not have the default value `fortls` AND // 2. is not an absolute path - if (executablePath !== 'fortls' && !path.isAbsolute(executablePath)) { + if (!path.isAbsolute(executablePath)) { this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.fsPath}`); executablePath = path.join(root.fsPath, executablePath); } @@ -388,6 +403,16 @@ export class FortlsClient { return executablePath; } + private async isFortlsPathDefault(path: string): Promise { + if (path === 'fortls') { + return true; + } + if (os.platform() === 'win32' && path === 'fortls.exe') { + return true; + } + return false; + } + /** * Restart the language server */ diff --git a/src/util/ms-python-api/jupyter/types.ts b/src/util/ms-python-api/jupyter/types.ts new file mode 100644 index 00000000..cdaf01cd --- /dev/null +++ b/src/util/ms-python-api/jupyter/types.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem } from 'vscode'; + +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick( + item: QuickPickItem, + backEnabled: boolean + ): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; diff --git a/src/util/ms-python-api/types.ts b/src/util/ms-python-api/types.ts new file mode 100644 index 00000000..1cb426d5 --- /dev/null +++ b/src/util/ms-python-api/types.ts @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; + +import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ + +export interface IExtensionApi { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + * @type {Promise} + * @memberof IExtensionApi + */ + ready: Promise; + jupyter: { + registerHooks(): void; + }; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param {string} host + * @param {number} port + * @param {boolean} [waitUntilDebuggerAttaches=true] + * @returns {Promise} + */ + getRemoteLauncherCommand( + host: string, + port: number, + waitUntilDebuggerAttaches: boolean + ): Promise; + + /** + * Gets the path to the debugger package used by the extension. + * @returns {Promise} + */ + getDebuggerPackagePath(): Promise; + }; + + datascience: { + /** + * Launches Data Viewer component. + * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param {string} title Data Viewer title + */ + showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; + /** + * Registers a remote server provider component that's used to pick remote jupyter server URIs + * @param serverProvider object called back when picking jupyter server URI + */ + registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Workspace folder the environment changed for. + */ + readonly resource: WorkspaceFolder | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; diff --git a/src/util/python-config.ts b/src/util/python-config.ts new file mode 100644 index 00000000..7935bc24 --- /dev/null +++ b/src/util/python-config.ts @@ -0,0 +1,8 @@ +'use strict'; + +import { Uri } from 'vscode'; + +import { Python } from './python'; + +// TODO: implement interface and config +export class PythonConfiguration {} diff --git a/src/util/python.ts b/src/util/python.ts new file mode 100644 index 00000000..3222df0b --- /dev/null +++ b/src/util/python.ts @@ -0,0 +1,100 @@ +import * as path from 'path'; + +import { extensions, Uri } from 'vscode'; + +import { IExtensionApi, ResolvedEnvironment } from './ms-python-api/types'; +import { shellTask, spawnAsPromise } from './shell'; + +export class Python { + public readonly path: Promise; + private usingMSPython = false; + private pythonEnvMS: ResolvedEnvironment | undefined; + + constructor(resource?: Uri) { + this.path = this.getPythonPath(resource); + } + + /** + * Get the path to the active Python interpreter. + * + * @returns The path to the active Python interpreter. + */ + public async getPythonPath(resource?: Uri): Promise { + const pythonEnv = await this.getPythonEnvMS(resource); + if (pythonEnv) { + this.usingMSPython = true; + return pythonEnv.path; + } + return process.platform === 'win32' ? 'python' : 'python3'; + } + + /** + * Get the path to the directory where pip installs binaries. + * + * @returns The path to the directory where pip installs binaries. + */ + public async getPipBinDir(): Promise { + const py = await this.path; + const script = path.join(__dirname, 'scripts', 'get_pip_bin_dir.py'); + const [stdout, stderr] = await spawnAsPromise(py, [script]); + if (stderr) { + throw new Error(stderr); + } + return stdout.trim(); + } + + /** + * Install a Python package using pip. + * + * @param packageName The name of the package to install. + * @returns The output of the pip command. + */ + public async pipInstall(packageName: string): Promise { + const py = await this.path; + const args = ['-m', 'pip', 'install', '--user', '--upgrade', packageName]; + return await shellTask(py, args, `pip: ${packageName}`); + } + + /** + * Get the active Python environment, if any, via the ms-python.python + * extension API. + * + * @returns The active Python environment, or undefined if there is none. + */ + public async getPythonEnvMS(resource?: Uri): Promise { + try { + const extension = extensions.getExtension('ms-python.python'); + if (!extension) { + return undefined; // extension not installed + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: IExtensionApi = extension.exports as IExtensionApi; + const activeEnv: ResolvedEnvironment = await pythonApi.environments.resolveEnvironment( + pythonApi.environments.getActiveEnvironmentPath(resource) + ); + if (!activeEnv) { + return undefined; // no active environment, unlikely but possible + } + this.pythonEnvMS = activeEnv; + return activeEnv; + } catch (error) { + return undefined; + } + } + + public async isInstalled(packageName: string): Promise { + const py = await this.path; + const script = path.join(__dirname, 'scripts', 'mod_in_env.py'); + try { + const [_, stderr] = await spawnAsPromise(py, [script, packageName]); + if (stderr) { + return false; + } + return true; + } catch (error) { + return false; + } + } +} diff --git a/src/util/shell.ts b/src/util/shell.ts new file mode 100644 index 00000000..a370c66b --- /dev/null +++ b/src/util/shell.ts @@ -0,0 +1,73 @@ +'use strict'; + +import * as cp from 'child_process'; + +import * as vscode from 'vscode'; + +export async function shellTask(command: string, args: string[], name: string): Promise { + const task = new vscode.Task( + { type: 'shell' }, + vscode.TaskScope.Workspace, + name, + 'Modern Fortran', + new vscode.ShellExecution(command, args) + ); + // Temporay fix to https://github.com/microsoft/vscode/issues/157756 + (task).definition = { type: 'shell', command: command }; + const execution = await vscode.tasks.executeTask(task); + return await new Promise((resolve, reject) => { + const disposable = vscode.tasks.onDidEndTaskProcess(e => { + if (e.execution === execution) { + disposable.dispose(); + if (e.exitCode !== 0) { + reject(`ERROR: ${e.execution.task.name} failed with code ${e.exitCode}`); + } + resolve(`${name}: shell task completed successfully.`); + } + }); + }); +} + +/** + * Spawn a command as a `Promise` + * @param cmd command to execute + * @param args arguments to pass to the command + * @param options child_process.spawn options + * @param input any input to pass to stdin + * @param ignoreExitCode ignore the exit code of the process and `resolve` the promise + * @returns Tuple[string, string] `[stdout, stderr]`. By default will `reject` if exit code is non-zero. + */ +export async function spawnAsPromise( + cmd: string, + args: ReadonlyArray | undefined, + options?: cp.SpawnOptions | undefined, + input?: string | undefined, + ignoreExitCode?: boolean +) { + return new Promise<[string, string]>((resolve, reject) => { + let stdout = ''; + let stderr = ''; + const child = cp.spawn(cmd, args, options); + child.stdout.on('data', data => { + stdout += data; + }); + child.stderr.on('data', data => { + stderr += data; + }); + child.on('close', code => { + if (ignoreExitCode || code === 0) { + resolve([stdout, stderr]); + } else { + reject([stdout, stderr]); + } + }); + child.on('error', err => { + reject(err.toString()); + }); + + if (input) { + child.stdin.write(input); + child.stdin.end(); + } + }); +} diff --git a/src/util/tools.ts b/src/util/tools.ts index 87df97cd..09704d7f 100644 --- a/src/util/tools.ts +++ b/src/util/tools.ts @@ -1,11 +1,11 @@ import * as assert from 'assert'; -import * as cp from 'child_process'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import { isString, isArrayOfString } from './helper'; +import { Python } from './python'; export const LS_NAME = 'fortls'; export const EXTENSION_ID = 'fortran'; @@ -119,7 +119,8 @@ export async function promptForMissingTool( if (selected === 'Install') { if (toolType === 'Python') { try { - const inst_msg = await pipInstall(tool); + const python = new Python(); + const inst_msg = await python.pipInstall(tool); vscode.window.showInformationMessage(inst_msg); } catch (error) { vscode.window.showErrorMessage(error); @@ -137,42 +138,6 @@ export async function promptForMissingTool( }); } -/** - * A wrapper around a call to `pip` for installing external tools. - * Does not explicitly check if `pip` is installed. - * - * @param pyPackage name of python package in PyPi - */ -export async function pipInstall(pyPackage: string): Promise { - const py = 'python3'; // Fetches the top-most python in the Shell - const args = ['-m', 'pip', 'install', '--user', '--upgrade', pyPackage]; - return await shellTask(py, args, `pip: ${pyPackage}`); -} - -export async function shellTask(command: string, args: string[], name: string): Promise { - const task = new vscode.Task( - { type: 'shell' }, - vscode.TaskScope.Workspace, - name, - 'Modern Fortran', - new vscode.ShellExecution(command, args) - ); - // Temporay fix to https://github.com/microsoft/vscode/issues/157756 - (task).definition = { type: 'shell', command: command }; - const execution = await vscode.tasks.executeTask(task); - return await new Promise((resolve, reject) => { - const disposable = vscode.tasks.onDidEndTaskProcess(e => { - if (e.execution === execution) { - disposable.dispose(); - if (e.exitCode !== 0) { - reject(`ERROR: ${e.execution.task.name} failed with code ${e.exitCode}`); - } - resolve(`${name}: shell task completed successfully.`); - } - }); - }); -} - /** * Resolve VSCode internal variables `workspaceFolder`, `env`, `config`, etc. * @@ -286,47 +251,3 @@ export function pathRelToAbs(relPath: string, uri: vscode.Uri): string | undefin export function getWholeFileRange(document: vscode.TextDocument): vscode.Range { return new vscode.Range(0, 0, document.lineCount, 0); } - -/** - * Spawn a command as a `Promise` - * @param cmd command to execute - * @param args arguments to pass to the command - * @param options child_process.spawn options - * @param input any input to pass to stdin - * @param ignoreExitCode ignore the exit code of the process and `resolve` the promise - * @returns Tuple[string, string] `[stdout, stderr]`. By default will `reject` if exit code is non-zero. - */ -export async function spawnAsPromise( - cmd: string, - args: ReadonlyArray | undefined, - options?: cp.SpawnOptions | undefined, - input?: string | undefined, - ignoreExitCode?: boolean -) { - return new Promise<[string, string]>((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const child = cp.spawn(cmd, args, options); - child.stdout.on('data', data => { - stdout += data; - }); - child.stderr.on('data', data => { - stderr += data; - }); - child.on('close', code => { - if (ignoreExitCode || code === 0) { - resolve([stdout, stderr]); - } else { - reject([stdout, stderr]); - } - }); - child.on('error', err => { - reject(err.toString()); - }); - - if (input) { - child.stdin.write(input); - child.stdin.end(); - } - }); -} diff --git a/test/integration/linter.test.ts b/test/integration/linter.test.ts index 2cb2106b..43dfacff 100644 --- a/test/integration/linter.test.ts +++ b/test/integration/linter.test.ts @@ -29,7 +29,8 @@ import { } from '../../src/lint/compilers'; import { FortranLintingProvider } from '../../src/lint/provider'; import { LogLevel, Logger } from '../../src/services/logging'; -import { EXTENSION_ID, pipInstall } from '../../src/util/tools'; +import { pipInstall } from '../../src/util/python'; +import { EXTENSION_ID } from '../../src/util/tools'; suite('Linter VS Code commands', async () => { let doc: TextDocument; diff --git a/test/unitTest/shell.test.ts b/test/unitTest/shell.test.ts new file mode 100644 index 00000000..bbfa0c91 --- /dev/null +++ b/test/unitTest/shell.test.ts @@ -0,0 +1,53 @@ +import * as assert from 'assert'; +import * as path from 'path'; + +import { shellTask, spawnAsPromise } from '../../src/util/shell'; + +suite('Tools tests', () => { + test('shellTask returns correct output', async () => { + const name = 'pip: fortls'; + const output = await shellTask( + 'python3', + ['-m', 'pip', 'install', '--upgrade', '--force', 'fortls'], + name + ); + assert.strictEqual(output, `${name}: shell task completed successfully.`); + }); + + test('shellTask returns rejected promise', async () => { + const name = 'pip: fortls'; + await assert.rejects(shellTask('python3', ['-m', 'pip', 'install', 'fortls2'], name)); + }); + + test('spawnAsPromise correct stdout, stderr output exit code 0', async () => { + const [stdout, stderr] = await spawnAsPromise('node', [ + path.resolve(__dirname, './exit-code.js'), + ]); + assert.strictEqual(stdout, 'Hello World!'); + assert.strictEqual(stderr, 'No errors'); + }); + + test('spawnAsPromise correct stdout, stderr output exit code 1', async () => { + try { + const [stdout, stderr] = await spawnAsPromise('node', [ + path.resolve(__dirname, './exit-code-err.js'), + ]); + } catch (error) { + const [stdout, stderr] = error; + assert.strictEqual(stdout, 'Hello World!'); + assert.strictEqual(stderr, 'Errors'); + } + }); + + test('spawnAsPromise correct stdout, stderr output exit code 1 with ignoreExitCode', async () => { + const [stdout, stderr] = await spawnAsPromise( + 'node', + [path.resolve(__dirname, './exit-code-err.js')], + undefined, + undefined, + true + ); + assert.strictEqual(stdout, 'Hello World!'); + assert.strictEqual(stderr, 'Errors'); + }); +}); diff --git a/test/unitTest/tools.test.ts b/test/unitTest/tools.test.ts index 7c59a2a7..5940791c 100644 --- a/test/unitTest/tools.test.ts +++ b/test/unitTest/tools.test.ts @@ -3,56 +3,9 @@ import * as path from 'path'; import { Uri } from 'vscode'; -import { shellTask, spawnAsPromise, pathRelToAbs } from '../../src/util/tools'; +import { pathRelToAbs } from '../../src/util/tools'; suite('Tools tests', () => { - test('shellTask returns correct output', async () => { - const name = 'pip: fortls'; - const output = await shellTask( - 'python3', - ['-m', 'pip', 'install', '--upgrade', '--force', 'fortls'], - name - ); - assert.strictEqual(output, `${name}: shell task completed successfully.`); - }); - - test('shellTask returns rejected promise', async () => { - const name = 'pip: fortls'; - await assert.rejects(shellTask('python3', ['-m', 'pip', 'install', 'fortls2'], name)); - }); - - test('spawnAsPromise correct stdout, stderr output exit code 0', async () => { - const [stdout, stderr] = await spawnAsPromise('node', [ - path.resolve(__dirname, './exit-code.js'), - ]); - assert.strictEqual(stdout, 'Hello World!'); - assert.strictEqual(stderr, 'No errors'); - }); - - test('spawnAsPromise correct stdout, stderr output exit code 1', async () => { - try { - const [stdout, stderr] = await spawnAsPromise('node', [ - path.resolve(__dirname, './exit-code-err.js'), - ]); - } catch (error) { - const [stdout, stderr] = error; - assert.strictEqual(stdout, 'Hello World!'); - assert.strictEqual(stderr, 'Errors'); - } - }); - - test('spawnAsPromise correct stdout, stderr output exit code 1 with ignoreExitCode', async () => { - const [stdout, stderr] = await spawnAsPromise( - 'node', - [path.resolve(__dirname, './exit-code-err.js')], - undefined, - undefined, - true - ); - assert.strictEqual(stdout, 'Hello World!'); - assert.strictEqual(stderr, 'Errors'); - }); - test('Resolve local paths: undefined', () => { const root = Uri.parse('/home/user/project'); const absPath = pathRelToAbs('./sample.f90', root); diff --git a/webpack.config.js b/webpack.config.js index 773a2cbf..ad2949c5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,6 +4,7 @@ const path = require('path'); const webpack = require('webpack'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); /**@type {import('webpack').Configuration}*/ const config = { @@ -25,6 +26,14 @@ const config = { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader extensions: ['.ts', '.js'], }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { from: 'scripts/get_pip_bin_dir.py', to: 'scripts/get_pip_bin_dir.py' }, + { from: 'scripts/mod_in_env.py', to: 'scripts/mod_in_env.py' }, + ], + }), + ], module: { rules: [ {