diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a20a9ac..e09fdbc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,7 @@ name: 'Build & Test' on: push: - + pull_request: branches: - main @@ -18,6 +18,8 @@ jobs: - uses: actions/setup-node@v1 with: node-version: '16.x' + - name: Install and setup conan + run: pip install conan && conan profile detect - run: npm install - run: npm run compile - - run: npm run test \ No newline at end of file + - run: npm run test diff --git a/CHANGELOG.md b/CHANGELOG.md index 5839429..6d91524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 1.3.0 - unreleased + +* [#36](https://github.com/afri-bit/vsconan/issues/36) Support the application of Conan's BuildEnv/RunEnv (Conan 2 only) + ## 1.2.0 - 2024-08-03 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8fe2d0..bec4a20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,4 +44,22 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue] By contributing, you agree that your contributions will be licensed under its MIT License. ## References -This document was adapted from the open-source contribution guidelines for [Facebook's Draft]() \ No newline at end of file +This document was adapted from the open-source contribution guidelines for [Facebook's Draft]() + +## Setup + +This repository follows the standard layout of a VS Code extension. +More information can be found [here](https://code.visualstudio.com/api/get-started/extension-anatomy). + +### Tests + +In addition to the standard setup for VS Code extension development [./test/conan/readEnv.test.ts](./test/conan/readEnv.test.ts) requires a proper Conan 2 installation. +This can be achieved e.g. by using a Python virtual environment: + +```sh +python -m venv .venv +. .venv/bin/activate +# or on Windows +# .venv/Scripts/activate +pip install "conan>=2" +``` diff --git a/README.md b/README.md index 2d21ad7..e07f67e 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,25 @@ The default configuration file can be seen as following. You can extend the list } ``` +#### Application of Conan's buildEnv/runEnv (currently Conan 2 only) + +VSConan provides the commands + +* `VSConan: Activate BuildEnv` +* `VSConan: Activate RunEnv` +* `VSConan: Deactivate BuildEnv/RunEnv` + +to adjust VSCode's process and terminal environment to the respective Conan environment. + +This is useful if you have tool dependencies in your Conanfile, e.g. CMake, a specific Compiler toolchain, etc and want to use these tools also in VSCode, e.g. the [CMake Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools). + +##### A note if using the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) in parallel + +The [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) overrides the `PATH` environment variable to add the currently selected Python interpreter. +In order to use `PATH` modifications by Conan BuildEnv/RunEnv the VSConan extension provides the option to generate a `.env`-file which is respected by the Python extension. + +This option is enabled by default and can be managed by `vsconan.conan.env.dotenv`. + ### Additional Support Features * `VSConan: Create Workspace Configuration (JSON)` diff --git a/doc/FEATURES.md b/doc/FEATURES.md index 039557e..08d8265 100644 --- a/doc/FEATURES.md +++ b/doc/FEATURES.md @@ -63,6 +63,7 @@ * Add editable package * Remove editable package * Automatic selection of Python interpreter using the ms-python.python extension +* Application of Conan's buildEnv/runEnv ## General * Define multiple conan profiles inside `settings.json` that you can use for the extension. diff --git a/package.json b/package.json index 9f83cfb..0ff266c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vsconan", "displayName": "VSConan", "description": "Conan local cache and workspace manager.", - "version": "1.2.0", + "version": "1.3.0", "publisher": "afri-bit", "repository": { "type": "git", @@ -122,6 +122,18 @@ "command": "vsconan.conan.profile.switch", "title": "VSConan: Switch Conan Profile" }, + { + "command": "vsconan.conan.buildenv", + "title": "VSConan: Activate BuildEnv" + }, + { + "command": "vsconan.conan.runenv", + "title": "VSConan: Activate RunEnv" + }, + { + "command": "vsconan.conan.deactivateenv", + "title": "VSConan: Deactivate BuildEnv/RunEnv" + }, { "command": "vsconan.config.workspace.create", "title": "VSConan: Create Workspace Configuration (JSON)" @@ -786,6 +798,11 @@ "type": "string", "default": "default", "markdownDescription": "Conan profile default / selection" + }, + "vsconan.conan.env.dotenv": { + "markdownDescription": "Manage `.env` file when activating Conan environments using `vsconan.conan.buildenv` or `vsconan.conan.runenv`. This is required if `ms-python.python` extension manages your terminal environment.", + "type": "boolean", + "default": true } } }, diff --git a/resources/print_env.py b/resources/print_env.py new file mode 100644 index 0000000..04f681e --- /dev/null +++ b/resources/print_env.py @@ -0,0 +1,88 @@ +import argparse +import os +import sys + +from conan.api import conan_api +from conan.api.output import ConanOutput +from conan.cli.args import common_graph_args, validate_common_graph_args +from conan.errors import ConanException +from conan.tools.env import VirtualBuildEnv, VirtualRunEnv + + +def print_env(conan_api, whichenv, args): + """ + Print requested environment. + + More or less a copy of https://github.com/conan-io/conan/blob/917ce14b5e4d9e9c7bb78c47fc0ba785f690f8ac/conan/cli/commands/install.py#L43 + """ + # basic paths + cwd = os.getcwd() + path = ( + conan_api.local.get_conanfile_path(args.path, cwd, py=None) + if args.path + else None + ) + + # Basic collaborators: remotes, lockfile, profiles + remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] + overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None + lockfile = conan_api.lockfile.get_lockfile( + lockfile=args.lockfile, + conanfile_path=path, + cwd=cwd, + partial=args.lockfile_partial, + overrides=overrides, + ) + profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) + + # Graph computation (without installation of binaries) + gapi = conan_api.graph + deps_graph = gapi.load_graph_consumer( + path, + args.name, + args.version, + args.user, + args.channel, + profile_host, + profile_build, + lockfile, + remotes, + args.update, + # is_build_require=args.build_require, + ) + gapi.analyze_binaries( + deps_graph, args.build, remotes, update=args.update, lockfile=lockfile + ) + # print_graph_packages(deps_graph) + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) + + conanfile = deps_graph.root.conanfile + + if whichenv == "BuildEnv": + env = VirtualBuildEnv(conanfile) + vars = env.vars(scope="build") + else: + env = VirtualRunEnv(conanfile) + vars = env.vars(scope="run") + return dict(vars.items()) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("whichenv", choices=("BuildEnv", "RunEnv")) + common_graph_args(parser) + args = parser.parse_args() + validate_common_graph_args(args) + if not args.path: + raise ConanException("Please specify a path to a conanfile") + args.no_remote = True + + ConanOutput.define_log_level("quiet") + env = print_env(conan_api.ConanAPI(), args.whichenv, args) + env["PATH"] = os.pathsep.join( + (os.path.dirname(sys.executable), env.get("PATH", os.environ["PATH"])) + ) + + import json + + print(json.dumps(env)) diff --git a/src/conans/conan2/api/conanAPI.ts b/src/conans/conan2/api/conanAPI.ts index b695084..63ff123 100644 --- a/src/conans/conan2/api/conanAPI.ts +++ b/src/conans/conan2/api/conanAPI.ts @@ -1,5 +1,6 @@ import { execSync } from "child_process"; import * as fs from "fs"; +import * as vscode from 'vscode'; import { ConanAPI, ConanExecutionMode } from "../../api/base/conanAPI"; import { RecipeFolderOption } from "../../conan/api/conanAPI"; import { ConanPackage } from "../../model/conanPackage"; @@ -47,8 +48,9 @@ export class Conan2API extends ConanAPI { } public override getConanHomePath(): string | undefined { + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; try { - let homePath = execSync(`${this.conanExecutor} config home`).toString(); + let homePath = execSync(`${this.conanExecutor} config home`, options).toString(); return homePath.trim(); // Remove whitespace and new lines } catch (err) { @@ -81,13 +83,15 @@ export class Conan2API extends ConanAPI { } public override getRecipePath(recipe: string): string | undefined { - let recipePath = execSync(`${this.conanExecutor} cache path ${recipe}`).toString().trim(); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + let recipePath = execSync(`${this.conanExecutor} cache path ${recipe}`, options).toString().trim(); return recipePath; } public override getPackagePath(recipe: string, packageId: string): string | undefined { - let packagePath = execSync(`${this.conanExecutor} cache path ${recipe}:${packageId}`).toString().trim(); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + let packagePath = execSync(`${this.conanExecutor} cache path ${recipe}:${packageId}`, options).toString().trim(); return packagePath; } @@ -96,7 +100,8 @@ export class Conan2API extends ConanAPI { let listOfRecipes: Array = []; try { - let jsonStdout = execSync(`${this.conanExecutor} list *#* --format json`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + let jsonStdout = execSync(`${this.conanExecutor} list *#* --format json`, options); let jsonObject = JSON.parse(jsonStdout.toString()); let localCache = jsonObject["Local Cache"]; @@ -118,7 +123,8 @@ export class Conan2API extends ConanAPI { public override getProfiles(): string[] { try { - let stdout = execSync(`${this.conanExecutor} profile list --format json`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + let stdout = execSync(`${this.conanExecutor} profile list --format json`, options); let jsonObject = JSON.parse(stdout.toString()); return jsonObject; } @@ -133,7 +139,8 @@ export class Conan2API extends ConanAPI { try { if (recipe) { - let jsonStdout = execSync(`${this.conanExecutor} list ${recipe}:* --format json`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + let jsonStdout = execSync(`${this.conanExecutor} list ${recipe}:* --format json`, options); let jsonObject = JSON.parse(jsonStdout.toString()); let recipeRevisionSplit = recipe.split("#"); @@ -202,11 +209,13 @@ export class Conan2API extends ConanAPI { } public override removePackage(recipe: string, packageId: string): void { - execSync(`${this.conanExecutor} remove ${recipe}:${packageId} -c`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remove ${recipe}:${packageId} -c`, options); } public override removeRecipe(recipe: string): void { - execSync(`${this.conanExecutor} remove ${recipe} -c`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remove ${recipe} -c`, options); } public override removeProfile(profile: string): void { @@ -222,28 +231,33 @@ export class Conan2API extends ConanAPI { } public override addRemote(remote: string, url: string): void { - execSync(`${this.conanExecutor} remote add ${remote} ${url}`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remote add ${remote} ${url}`, options); } public override removeRemote(remote: string): void { - execSync(`${this.conanExecutor} remote remove ${remote}`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remote remove ${remote}`, options); } public override enableRemote(remote: string, enable: boolean): void { + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; if (enable) { - execSync(`${this.conanExecutor} remote enable ${remote}`); + execSync(`${this.conanExecutor} remote enable ${remote}`, options); } else { - execSync(`${this.conanExecutor} remote disable ${remote}`); + execSync(`${this.conanExecutor} remote disable ${remote}`, options); } } public override renameRemote(remoteName: string, newName: string): void { - execSync(`${this.conanExecutor} remote rename ${remoteName} ${newName}`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remote rename ${remoteName} ${newName}`, options); } public override updateRemoteURL(remoteName: string, url: string): void { - execSync(`${this.conanExecutor} remote update ${remoteName} --url ${url}`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remote update ${remoteName} --url ${url}`, options); } public override renameProfile(oldProfileName: string, newProfileName: string): void { @@ -324,7 +338,8 @@ export class Conan2API extends ConanAPI { try { if (recipe && packageId) { - let jsonStdout = execSync(`${this.conanExecutor} list ${recipe}:${packageId}#* --format json`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + let jsonStdout = execSync(`${this.conanExecutor} list ${recipe}:${packageId}#* --format json`, options); let jsonObject = JSON.parse(jsonStdout.toString()); let recipeRevisionSplit = recipe.split("#"); @@ -356,7 +371,8 @@ export class Conan2API extends ConanAPI { let packageRevisionPath: string | undefined = undefined; try { - packageRevisionPath = execSync(`${this.conanExecutor} cache path ${recipe}:${packageId}#${revisionId}`).toString().trim(); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + packageRevisionPath = execSync(`${this.conanExecutor} cache path ${recipe}:${packageId}#${revisionId}`, options).toString().trim(); } catch (err) { console.log((err as Error).message); @@ -366,6 +382,7 @@ export class Conan2API extends ConanAPI { } public removePackageRevision(recipe: string, packageId: string, revisionId: string): void { - execSync(`${this.conanExecutor} remove ${recipe}:${packageId}#${revisionId} -c`); + const options = { 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + execSync(`${this.conanExecutor} remove ${recipe}:${packageId}#${revisionId} -c`, options); } -} \ No newline at end of file +} diff --git a/src/extension/manager/vsconanWorkspace.ts b/src/extension/manager/vsconanWorkspace.ts index 041a571..f98d1e3 100644 --- a/src/extension/manager/vsconanWorkspace.ts +++ b/src/extension/manager/vsconanWorkspace.ts @@ -11,7 +11,7 @@ import * as utils from '../../utils/utils'; import { ConanProfileConfiguration } from "../settings/model"; import { SettingsPropertyManager } from "../settings/settingsPropertyManager"; import { ExtensionManager } from "./extensionManager"; - +import { VSConanWorkspaceEnvironment } from "./workspaceEnvironment"; enum ConanCommand { create, @@ -19,7 +19,10 @@ enum ConanCommand { build, source, package, - packageExport + packageExport, + activateBuildEnv, + activateRunEnv, + deactivateEnv } interface ConfigCommandQuickPickItem extends vscode.QuickPickItem { @@ -34,6 +37,7 @@ export class VSConanWorkspaceManager extends ExtensionManager { private outputChannel: vscode.OutputChannel; private conanApiManager: ConanAPIManager; private settingsPropertyManager: SettingsPropertyManager; + private workspaceEnvironment: VSConanWorkspaceEnvironment; private statusBarConanVersion: vscode.StatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); @@ -53,6 +57,7 @@ export class VSConanWorkspaceManager extends ExtensionManager { this.outputChannel = outputChannel; this.conanApiManager = conanApiManager; this.settingsPropertyManager = settingsPropertyManager; + this.workspaceEnvironment = new VSConanWorkspaceEnvironment(context, settingsPropertyManager, outputChannel); this.registerCommand("vsconan.conan.create", () => this.executeConanCommand(ConanCommand.create)); this.registerCommand("vsconan.conan.install", () => this.executeConanCommand(ConanCommand.install)); @@ -65,6 +70,9 @@ export class VSConanWorkspaceManager extends ExtensionManager { this.registerCommand("vsconan.config.workspace.create", () => this.createWorkspaceConfig()); this.registerCommand("vsconan.config.workspace.open", () => this.openWorkspaceConfig()); this.registerCommand("vsconan.conan.profile.switch", () => this.switchConanProfile()); + this.registerCommand("vsconan.conan.buildenv", () => this.executeConanCommand(ConanCommand.activateBuildEnv)); + this.registerCommand("vsconan.conan.runenv", () => this.executeConanCommand(ConanCommand.activateRunEnv)); + this.registerCommand("vsconan.conan.deactivateenv", () => this.executeConanCommand(ConanCommand.deactivateEnv)); this.initStatusBarConanVersion(); @@ -86,7 +94,9 @@ export class VSConanWorkspaceManager extends ExtensionManager { let selectedProfile: string | undefined = this.settingsPropertyManager.getSelectedConanProfile(); this.settingsPropertyManager.getConanProfileObject(selectedProfile!).then(selectedProfileObject => { if (selectedProfileObject && selectedProfileObject.isValid()) { - this.statusBarConanVersion.text = `$(extensions) VSConan | conan${selectedProfileObject.conanVersion} - ${selectedProfile}`; + const activeEnv = this.workspaceEnvironment.activeEnv(); + const activeEnvStr = activeEnv ? ` - ${activeEnv[1]}[${activeEnv[2]}]` : ''; + this.statusBarConanVersion.text = `$(extensions) VSConan | conan${selectedProfileObject.conanVersion} - ${selectedProfile}${activeEnvStr}`; this.statusBarConanVersion.color = ""; this.statusBarConanVersion.tooltip = new vscode.MarkdownString(`### Python Interpreter\n\`${selectedProfileObject.conanPythonInterpreter}\`\n### Conan Executable\n\`${selectedProfileObject.conanExecutable}\``); } @@ -202,6 +212,7 @@ export class VSConanWorkspaceManager extends ExtensionManager { let conanCommand = ""; let commandBuilder: CommandBuilder | undefined; let conanVersion: string | null = ""; + let conanProfileObject: ConanProfileConfiguration | undefined; // Get current profile let currentConanProfile = this.settingsPropertyManager.getSelectedConanProfile(); @@ -210,7 +221,7 @@ export class VSConanWorkspaceManager extends ExtensionManager { conanVersion = await this.settingsPropertyManager.getConanVersionOfProfile(currentConanProfile!); commandBuilder = CommandBuilderFactory.getCommandBuilder(conanVersion!); - let conanProfileObject: ConanProfileConfiguration | undefined = await this.settingsPropertyManager.getConanProfileObject(currentConanProfile!); + conanProfileObject = await this.settingsPropertyManager.getConanProfileObject(currentConanProfile!); if (conanProfileObject?.conanExecutionMode === "pythonInterpreter" && conanProfileObject.conanPythonInterpreter) { conanCommand = `${conanProfileObject.conanPythonInterpreter} -m conans.conan`; @@ -258,6 +269,30 @@ export class VSConanWorkspaceManager extends ExtensionManager { this.executeCommandConanPackageExport(wsPath!, conanCommand, commandBuilder!, configWorkspace.commandContainer.pkgExport); break; } + case ConanCommand.activateBuildEnv: { + if (conanVersion === "1") { + vscode.window.showErrorMessage("This command is not yet supported for Conan 1"); + break; + } + this.executeCommandActivateEnv(wsPath!, conanProfileObject.conanPythonInterpreter, utils.conan.ConanEnv.buildEnv, commandBuilder!, configWorkspace.commandContainer.install); + break; + } + case ConanCommand.activateRunEnv: { + if (conanVersion === "1") { + vscode.window.showErrorMessage("This command is not yet supported for Conan 1"); + break; + } + this.executeCommandActivateEnv(wsPath!, conanProfileObject.conanPythonInterpreter, utils.conan.ConanEnv.runEnv, commandBuilder!, configWorkspace.commandContainer.install); + break; + } + case ConanCommand.deactivateEnv: { + if (conanVersion === "1") { + vscode.window.showErrorMessage("This command is not yet supported for Conan 1"); + break; + } + this.executeCommandDeactivateEnv(); + break; + } } } else { @@ -333,6 +368,36 @@ export class VSConanWorkspaceManager extends ExtensionManager { }); } + /** + * Deactivate Conan environment; i.e. restore original environment variables. + */ + private executeCommandDeactivateEnv() { + this.workspaceEnvironment.restoreEnvironment(); + this.updateStatusBar(); + } + + /** + * Activate given Conan environment. + * + * @param wsPath Absolute path of the workspace + * @param pythonInterpreter Python interpreter + * @param conanEnv Which Conan environment to activate + * @param commandBuilder Builder for Conan commands + * @param configList List of possible configurations + */ + private executeCommandActivateEnv(wsPath: string, pythonInterpreter: string, whichEnv: utils.conan.ConanEnv, commandBuilder: CommandBuilder, configList: Array) { + let promiseIndex = this.getCommandConfigIndex(configList); + + promiseIndex.then(index => { + if (index !== undefined) { + let selectedConfig = configList[index]; + let cmd = commandBuilder.buildCommandInstall(wsPath, selectedConfig); + cmd = cmd?.slice(1) ?? []; // cut of "install" from cmd + this.workspaceEnvironment.activateEnvironment(whichEnv, selectedConfig.name, pythonInterpreter, cmd).then(this.updateStatusBar); + } + }); + } + /** * Execute the 'conan install' command * @param wsPath Absolute path of the workspace diff --git a/src/extension/manager/workspaceEnvironment.ts b/src/extension/manager/workspaceEnvironment.ts new file mode 100644 index 0000000..1761b0a --- /dev/null +++ b/src/extension/manager/workspaceEnvironment.ts @@ -0,0 +1,146 @@ +import * as fs from 'fs'; +import * as vscode from 'vscode'; +import * as utils from '../../utils/utils'; +import { SettingsPropertyManager } from '../settings/settingsPropertyManager'; +import path = require('path'); + +/** + * Shorthand type for Array of "key=value" pairs of environment variables. + */ +type EnvVars = Array<[string, string | undefined]>; + +/** + * Tag workspace Metadata with version for easy upgrades. + */ +const activeEnvVersion: number = 1; +type ActiveEnv = [version: number, configName: string, conanEnv: utils.conan.ConanEnv, envValues: [string, string][]]; + + +/** + * Manage VSCode's process and terminal environment. + */ +export class VSConanWorkspaceEnvironment { + private context: vscode.ExtensionContext; + private settingsPropertyManager: SettingsPropertyManager; + private outputChannel: vscode.OutputChannel; + + public constructor(context: vscode.ExtensionContext, settingsPropertyManager: SettingsPropertyManager, outputChannel: vscode.OutputChannel) { + this.context = context; + this.settingsPropertyManager = settingsPropertyManager; + this.outputChannel = outputChannel; + + const activeEnv = this.activeEnv(); + if (activeEnv) { + this.updateVSCodeEnvironment(activeEnv[3]); + } + } + + /** + * Extend VSCode's environment by environment variables from Conan. + * + * @param conanEnv Which Conan environment to activate + * @param configName Config name + * @param pythonInterpreter Path to python interpreter + * @param args Additional Conan arguments as given to `conan install` + */ + public async activateEnvironment(conanEnv: utils.conan.ConanEnv, configName: string, pythonInterpreter: string, args: string[]) { + this.restoreEnvironment(); + var newenv = await utils.conan.readEnvFromConan(conanEnv, pythonInterpreter, args); + this.updateBackupEnvironment(newenv); + + this.updateVSCodeEnvironment(newenv); + const activeEnv: ActiveEnv = [activeEnvVersion, configName, conanEnv, newenv]; + await this.context.workspaceState.update("vsconan.activeEnv", activeEnv); + if (vscode.env.remoteName === undefined) { + await vscode.commands.executeCommand('workbench.action.restartExtensionHost'); + } else { + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + await this.outputChannel.appendLine(`Activate ${conanEnv}: ${JSON.stringify(newenv, null, 2)}`); + await vscode.window.showInformationMessage(`Activated Environment ${configName}[${conanEnv}]`); + } + + /** + * Restore VSCode environment using backup. + */ + public restoreEnvironment() { + const backupEnv = this.context.workspaceState.get("vsconan.backupEnv"); + console.log(`[vsconan] restoreEnvironment: ${backupEnv}`); + if (backupEnv) { + this.updateVSCodeEnvironment(backupEnv); + } + this.updateDotEnvFile([]); + this.context.workspaceState.update("vsconan.activeEnv", undefined); + } + + /** + * Update backup environment by saving all _current_ environment variables + * which would be modified by `newenv`. + * + * @param newenv New environment. Not the backup! + */ + private updateBackupEnvironment(newenv: EnvVars) { + let backupEnv = new Map(this.context.workspaceState.get("vsconan.backupEnv")); + let newBackupEnv: EnvVars = []; + newenv.forEach(([key, _]) => { + if (backupEnv.has(key)) { + newBackupEnv.push([key, backupEnv.get(key)]); + } else { + // TODO: Take really from process env?? + newBackupEnv.push([key, process.env[key]]); + } + }); + this.context.workspaceState.update("vsconan.backupEnv", newBackupEnv); + console.log(`[vsconan] updateBackupEnvironment: ${newBackupEnv}`); + } + + /** + * Update VSCode's process and terminal environment. + * + * @param data Environment variables to apply + */ + private updateVSCodeEnvironment(data: EnvVars) { + console.log(`[vsconan] updateVSCodeEnvironment: ${data}`); + data.forEach(([key, value]) => { + if (!value) { + delete process.env[key]; + this.context.environmentVariableCollection.delete(key); + } else { + process.env[key] = value; + this.context.environmentVariableCollection.replace(key, value); + } + }); + + this.updateDotEnvFile(data); + } + + /** + * Update `.env`-File in current Workspace if option is selected. + * + * @param data New content + */ + private updateDotEnvFile(data: EnvVars) { + if (this.settingsPropertyManager.isUpdateDotEnv() !== true) { + return; + } + let ws = utils.workspace.selectWorkspace(); + ws.then(result => { + const dotenv = path.join(String(result), ".env"); + const content = data.map(([key, value]) => { + return `${key}=${value}`; + }).join('\n'); + fs.writeFileSync(dotenv, content); + }).catch(reject => { + vscode.window.showInformationMessage("No workspace detected."); + }); + } + + public activeEnv(): ActiveEnv | undefined { + const activeEnv = this.context.workspaceState.get("vsconan.activeEnv"); + if (activeEnv?.[0] === activeEnvVersion) { + return activeEnv; + } + return undefined; + } + +} diff --git a/src/extension/settings/settingsPropertyManager.ts b/src/extension/settings/settingsPropertyManager.ts index 5eca8dc..7d5b6b5 100644 --- a/src/extension/settings/settingsPropertyManager.ts +++ b/src/extension/settings/settingsPropertyManager.ts @@ -129,6 +129,17 @@ export class SettingsPropertyManager { } } + let pythonInterpreter = await python.getCurrentPythonInterpreter(); + if (pythonInterpreter !== undefined) { + if (!profileObject?.conanPythonInterpreter) { + profileObject!.conanPythonInterpreter = pythonInterpreter; + } + if (!profileObject?.conanExecutable) { + const exePostfix = process.platform === 'win32' ? '.exe' : ''; + profileObject!.conanExecutable = path.join(path.dirname(pythonInterpreter), `conan${exePostfix}`); + } + } + return profileObject; } @@ -176,4 +187,8 @@ export class SettingsPropertyManager { return isAvailable; } + + public isUpdateDotEnv(): boolean | undefined { + return vscode.workspace.getConfiguration("vsconan.conan").get("env.dotenv"); + } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index c604141..cf91a8e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,16 +1,16 @@ -import * as vscode from "vscode"; -import * as path from "path"; -import * as os from "os"; -import * as constants from "./constants"; +import { PythonExtension } from '@vscode/python-extension'; +import { execSync, spawn } from "child_process"; import * as fs from "fs"; -import { ConfigWorkspace } from "../conans/workspace/configWorkspace"; -import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension'; +import * as os from "os"; +import * as path from "path"; +import * as vscode from "vscode"; import { CommandContainer, ConfigCommandBuild, ConfigCommandCreate, ConfigCommandInstall, ConfigCommandPackage, ConfigCommandPackageExport, ConfigCommandSource } from "../conans/command/configCommand"; -import { spawn } from "child_process"; +import { ConfigWorkspace } from "../conans/workspace/configWorkspace"; +import * as constants from "./constants"; export namespace vsconan { /** @@ -64,8 +64,9 @@ export namespace vsconan { // const exec = util.promisify(require('child_process').exec); // const { stdout, stderr } = await spawn(cmd); channel.show(); + channel.appendLine(`Executing: "${cmd} ${args.join(' ')}`); - const ls = spawn(cmd, args, { shell: true }); + const ls = spawn(cmd, args, { shell: true, 'cwd': vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }); ls.stdout.on("data", data => { channel.append(`${data}`); @@ -111,7 +112,15 @@ export namespace vsconan { export namespace conan { /** - * Utility function to determine whether a folder is a conan project + * Enum to distinguish between different Conan environments. + */ + export enum ConanEnv { + buildEnv = "BuildEnv", + runEnv = "RunEnv" + } + + /** + * Utility function to determine whether a folder is a conan project * by checking if the folder contains conanfile.py or conanfile.txt * @param ws Absolute path to workspace to be checked * @returns 'true' the path contains conanfile.py or conanfile.txt, otherwise 'false' @@ -128,13 +137,37 @@ export namespace conan { return ret; } + + /** + * Read environment variables from Conan's VirtualBuildEnv/VirtualRunEnv. + * + * @param conanEnv Which environment to generate + * @param pythonInterpreter Path to python interpreter + * @param args Additional Conan arguments as given to `conan install` + * @returns Array of environment settings + */ + export async function readEnvFromConan(conanEnv: ConanEnv, pythonInterpreter: string, args: string[]): Promise<[string, string][]> { + const envScript = path.join(path.dirname(__dirname), '..', 'resources', 'print_env.py'); + const options = { timeout: 20000, cwd: vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined }; + + const cmd = `${pythonInterpreter} ${envScript} ${conanEnv} ${args.join(' ')}`; + try { + const output = execSync(cmd, options); + const parsed = JSON.parse(`${output}`); + return Object.entries(parsed); + } catch (err) { + vscode.window.showErrorMessage((err as Error).message); + throw err; + } + } + } export namespace editor { /** * Function to open file in the editor, with or without workspace. * This function is just to simplify the mechanism of opening file in the editor - * + * * @param filePath File path to be opened */ export async function openFileInEditor(filePath: string) { @@ -147,8 +180,8 @@ export namespace editor { export namespace workspace { /** * Function to show quick pick to get a workspace path. - * This can list the multiple workspaces and user can select it using a quick pick menu. - * + * This can list the multiple workspaces and user can select it using a quick pick menu. + * * @returns Promise Selected workspace path or undefined */ export async function selectWorkspace(): Promise { @@ -195,10 +228,10 @@ export namespace workspace { * Helper function to get absolute path in relative to workspace path * If the path to be merged with workspace path is absolute it will return that path itself. * If the path is not absolute, it will return absolute path which is merge with workspace path. - * + * * @param wsPath Absolute path from workspace * @param pathName Path to be merged with workspace - * @returns + * @returns */ export function getAbsolutePathFromWorkspace(wsPath: string, pathName: string): string { if (path.isAbsolute(pathName)) { // Absolute path from the path itself diff --git a/test/conan/readEnv.test.ts b/test/conan/readEnv.test.ts new file mode 100644 index 0000000..68a5441 --- /dev/null +++ b/test/conan/readEnv.test.ts @@ -0,0 +1,36 @@ + +import { execSync } from "child_process"; +import { + conan +} from "../../src/utils/utils"; +import path = require("path"); +import fs = require('fs'); + + +jest.mock('vscode', () => ({ + workspace: { + workspaceFolders: [{ uri: { fsPath: __dirname } }], + } +}), { virtual: true }); + + +describe("readEnvFromConan ", () => { + it("should return the env including PATH", async () => { + execSync(`cd ${__dirname} && conan new basic --force`); + let env = await conan.readEnvFromConan(conan.ConanEnv.buildEnv, "python", ["conanfile.py"]); + expect(env).toBeInstanceOf(Array); + expect(env[0]).toContain("PATH"); + }); + + it("should contain custom settings", async () => { + execSync(`cd ${__dirname} && conan new basic --force`); + fs.appendFileSync(path.join(__dirname, "conanfile.py"), + ' def configure(self):\n' + + ' self.buildenv.define("FOO", "BAR")\n' + + ' self.runenv.define("BAR", "BAZ")\n'); + const buildenv = await conan.readEnvFromConan(conan.ConanEnv.buildEnv, "python", ["conanfile.py"]); + expect(buildenv[0]).toStrictEqual(["FOO", "BAR"]); + const runenv = await conan.readEnvFromConan(conan.ConanEnv.runEnv, "python", ["conanfile.py"]); + expect(runenv[0]).toStrictEqual(["BAR", "BAZ"]); + }); +});