From a60d2104169f89ea954be13ca56bfcca0ec1350b Mon Sep 17 00:00:00 2001 From: Stefan Hackenberg Date: Mon, 12 Aug 2024 12:04:57 +0200 Subject: [PATCH] Support the application of Conan's BuildEnv/RunEnv --- .github/workflows/build-and-test.yml | 6 +- CHANGELOG.md | 26 ++-- README.md | 89 ++++++----- doc/FEATURES.md | 7 +- package.json | 19 ++- resources/print_env.py | 88 +++++++++++ src/conans/conan2/api/conanAPI.ts | 53 ++++--- src/extension/manager/vsconanWorkspace.ts | 73 ++++++++- src/extension/manager/workspaceEnvironment.ts | 146 ++++++++++++++++++ .../settings/settingsPropertyManager.ts | 15 ++ src/utils/utils.ts | 61 ++++++-- test/conan/readEnv.test.ts | 36 +++++ 12 files changed, 531 insertions(+), 88 deletions(-) create mode 100644 resources/print_env.py create mode 100644 src/extension/manager/workspaceEnvironment.ts create mode 100644 test/conan/readEnv.test.ts 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..dd3e951 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 @@ -10,18 +14,18 @@ ### Added -* [#38](https://github.com/afri-bit/vsconan/issues/38) Support whitespace for project and configuration path +* [#38](https://github.com/afri-bit/vsconan/issues/38) Support whitespace for project and configuration path You can now use whitespace in your configuration file and *VSConan* can still parse the path and use it for executing conan command. - > Additional to this feature, internal the command builder is changed to separate the command and arguments. For further detail of the issue please refer to [#38](https://github.com/afri-bit/vsconan/issues/38). + > Additional to this feature, internal the command builder is changed to separate the command and arguments. For further detail of the issue please refer to [#38](https://github.com/afri-bit/vsconan/issues/38). *This change should not affect the current configuration file.* - + Thanks to [torsknod-the-caridian](https://github.com/torsknod-the-caridian). ## 1.0.1 - 2024-02-04 ### Changed -* [#35](https://github.com/afri-bit/vsconan/issues/35) Extension cannot be activated after installation +* [#35](https://github.com/afri-bit/vsconan/issues/35) Extension cannot be activated after installation Due to missing dependencies or unability to find the dependencies, the extension cannot be started. So I replaced the small function that I used from this dependency with internal utility function. > Midnight programming mistake :P @@ -58,7 +62,7 @@ Using `vsconan.conan.profile.default` you can switch the profile easily, in case you have multiple conan setup or multiple python virtual environments with different conan versions. `conanUserHome` is optional parameter, in case you want to have a different location for `conan` home folder. -* The workspace configuration to execute conan command, such as `build`, `install`, etc., is slightly changed, but it has a big impact to your workflow / usage of this configuration. +* The workspace configuration to execute conan command, such as `build`, `install`, etc., is slightly changed, but it has a big impact to your workflow / usage of this configuration. > The `python` attribute is no longer available! Instead of using hard coded `python` path inside this json file, it will rely on the selected conan configuration profile. So with this change, you can use the same json file but using in the different conan version (Easy switch between conan 1 and 2). @@ -163,9 +167,9 @@ ## 0.2.0 - 2022-05-30 ### Added -* [#10](https://github.com/afri-bit/vsconan/issues/10) Enable option to list dirty packages from a recipe +* [#10](https://github.com/afri-bit/vsconan/issues/10) Enable option to list dirty packages from a recipe * `vsconan.explorer.treeview.package.showDirtyPackage` is available to set the flag persistent -* [#14](https://github.com/afri-bit/vsconan/issues/14) Support non-pip conan installation +* [#14](https://github.com/afri-bit/vsconan/issues/14) Support non-pip conan installation * Enable possibility for user to use the extension using alternative conan installation (e.g. conan executable) * Provide mode switch between python interpreter and conan executable (User can still use the python interpreter to execute conan CLI) * Configuration for the extension in `settings.json` @@ -181,7 +185,7 @@ * Copy editable path to clipboard * Remove editable package via command and quickpick (simple option) * Add editable package from the workspace - * Enable layout file input for the editable package + * Enable layout file input for the editable package **!!!** Currently only supporting the manual input from the user for the layout. @@ -189,9 +193,9 @@ * The configuration for extension is migrated to official VS Code `settings.json`. Custom global `config.json` under `~/.vsconan` is now **deprecated**. ### Removed -* `VSConan: Create Global Configuration (JSON)` +* `VSConan: Create Global Configuration (JSON)` Command to create global configuration file in your home directory -* `VSConan: Open Global Configuration (JSON)` +* `VSConan: Open Global Configuration (JSON)` Open the global configuration file in editor ## 0.1.0 - 2022-04-21 @@ -231,4 +235,4 @@ * Create global configuration file * Open global configuration file * Create workspace configuration file - * Open workspace configuration file \ No newline at end of file + * Open workspace configuration file diff --git a/README.md b/README.md index 2d21ad7..437e3d2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@

-## Introduction +## Introduction The **VSConan** extension helps you manage the conan local cache on your machine. It gives you easy access to your local cache and allows you to manage it by using integrated explorer in the Visual Studio Code without typing a single line of command in the terminal. **VSConan** provides variety of features, including a quick overview of installed packages, renaming and duplicating profiles, enabling and disabling remotes and more. For more information see [Extension Features](#extension-features). @@ -33,7 +33,7 @@ The **VSConan** extension helps you manage the conan local cache on your machine **VSConan** contributes to official VS Code configurations (`settings.json`), where you can configure the environment to use this extension. As a starting point you can configure following settings, that are the core settings and provide you a high flexibility to use this extension: - * `vsconan.conan.profile.configurations` + * `vsconan.conan.profile.configurations` In this section of settings you can store multiple configuration profiles, that contain necessary information to use `conan` from your system. Let's take a look at the following example: ```json "vsconan.conan.profile.configurations": { @@ -53,13 +53,13 @@ As a starting point you can configure following settings, that are the core sett } ``` - In the example above, we defined the `foo` and `bar` profile to start using this extension. Each profile has different configuration for the python interpreter and the conan executable. One thing that we notice here is that we can select the conan version, we want to use, `1` or `2`. This information is crucial for the extension in order to get the correct API. + In the example above, we defined the `foo` and `bar` profile to start using this extension. Each profile has different configuration for the python interpreter and the conan executable. One thing that we notice here is that we can select the conan version, we want to use, `1` or `2`. This information is crucial for the extension in order to get the correct API. > **NOTE**: Make sure you combine the `conanVersion` and its binary accordingly. Otherwise it will have strange behaviours or things might even not work properly. - Using `conanUserHome` we can overwrite the current conan user home directory. This attribute is optional and has default value of `null`. In the example of `foo`, `conanUserHome` is not defined, which means that the conan user home directory uses the default path or predefined environment variable (See [Environment Variables](https://docs.conan.io/2/reference/environment.html)). - - * `vsconan.conan.profile.default` + Using `conanUserHome` we can overwrite the current conan user home directory. This attribute is optional and has default value of `null`. In the example of `foo`, `conanUserHome` is not defined, which means that the conan user home directory uses the default path or predefined environment variable (See [Environment Variables](https://docs.conan.io/2/reference/environment.html)). + + * `vsconan.conan.profile.default` After defining `foo` and `bar` profiles, now it is time for us to choose which configuration we want to use currently. ```json @@ -77,18 +77,18 @@ The **VSConan** extension contributes a Conan Explorer view to VS Code. The Cona #### Conan Recipe -In the Conan Recipe explorer you can have an overview of the installed conan recipe in your local cache. +In the Conan Recipe explorer you can have an overview of the installed conan recipe in your local cache. ![](https://raw.githubusercontent.com/afri-bit/vsconan/main/resources/img/conan_recipe_treeview.png) As you can see in the picture above, there are several inline options on each item in the treeview. -* _Information_ - Open a web view in VS Code editor, that contains information about this selected recipe. Currently the web view only shows a plain JSON text, that is obtained from the Conan CLI. -* _Open in Explorer_ +* _Information_ + Open a web view in VS Code editor, that contains information about this selected recipe. Currently the web view only shows a plain JSON text, that is obtained from the Conan CLI. +* _Open in Explorer_ Open the the recipe path in the explorer -* _Open in VS Code_ +* _Open in VS Code_ Open the selected recipe in a new VS Code window -* _Remove_ +* _Remove_ Remove the selected recipe #### Conan Binary Package @@ -97,11 +97,11 @@ By selecting the recipe, the corresponded binary packages will be shown in this ![](https://raw.githubusercontent.com/afri-bit/vsconan/main/resources/img/conan_package_treeview.png) Each item of this treeview has following options to offer: -* _Open in Explorer_ +* _Open in Explorer_ Open the selected binary package in the explorer -* _Open in VS Code_ +* _Open in VS Code_ Open the selected binary package in a new VS Code window -* _Remove_ +* _Remove_ Remove the selected binary package #### Conan Profile @@ -111,43 +111,43 @@ All the profiles that you saved on your machine will be listed in this explorer. ![](https://raw.githubusercontent.com/afri-bit/vsconan/main/resources/img/conan_profile_treeview.png) As the other treeviews, each item of this treeview contains several functionalities: -* _Edit_ +* _Edit_ Open the selected profile in the VS Code editor -* _Open in Explorer_ +* _Open in Explorer_ Open the selected profile in the file explorer -* _Rename_ +* _Rename_ Rename the selected profile -* _Duplicate_ +* _Duplicate_ If you want to change a small detail from a certain profile but you do not want to lose the original profile, we provide you this duplicate option to fulfill your purpose. -* _Remove_ +* _Remove_ Remove the selected profile #### Conan Remote -Finally we come to the last part of this explorer, which is the explorer of the conan remote. +Finally we come to the last part of this explorer, which is the explorer of the conan remote. The explorer itself provides you following options: -* _Edit_ +* _Edit_ Since the collection of remotes in conan is defined in one file called `remotes.json`, this option is not available of each remote item in the treeview. This will open `remotes.json` file in the VS Code editor instead. -* _Add_ +* _Add_ Add a new remote ![](https://raw.githubusercontent.com/afri-bit/vsconan/main/resources/img/conan_remote_treeview.png) As other treeview, each item is equipped with several options, that you can use to maintain your remotes. -* _Rename Remote_ +* _Rename Remote_ Rename the selected remote -* _Update URL_ +* _Update URL_ Modify the URL in the selected remote -* _Enable Remote_ - Enable the selected remote. Enabled remotes can be seen from the icon next to the remote name. The remote `conancenter` in the picture above is enabled. -* _Disable Remote_ - Disable the selected remote. Disabled remotes can be seen from the icon next to the remote name. The remote `anyOtherRemote` in the picture above is disabled. -* _Remove Remote_ +* _Enable Remote_ + Enable the selected remote. Enabled remotes can be seen from the icon next to the remote name. The remote `conancenter` in the picture above is enabled. +* _Disable Remote_ + Disable the selected remote. Disabled remotes can be seen from the icon next to the remote name. The remote `anyOtherRemote` in the picture above is disabled. +* _Remove Remote_ Remove the selected remote ### Conan Workspace -The Conan Workspace feature provides you configuration file, that can be used to execute predefined conan flow command and its arguments. The configuration will be stored under `.vsconan` folder in your workspace. +The Conan Workspace feature provides you configuration file, that can be used to execute predefined conan flow command and its arguments. The configuration will be stored under `.vsconan` folder in your workspace. If you work a lot with conan and use VS Code as your IDE, this feature can be really beneficial for you. It can spare you some seconds by avoiding to type same command, maybe with different arguments in your terminal over and over again. Instead you can save the command that you want to execute in the configuration and reuse in the next execution. In addition to that, the configuration file is reusable, and can be distributed to other people, if you work in a team. ![Recording of VSConan Workspace](https://raw.githubusercontent.com/afri-bit/vsconan/main/resources/img/demo_workspace.gif) @@ -156,7 +156,7 @@ If you work a lot with conan and use VS Code as your IDE, this feature can be re ![](https://raw.githubusercontent.com/afri-bit/vsconan/main/resources/img/prompt_conan_project.png) -If you choose yes, **VSConan** will generate a default configuration file in your workspace to start with. +If you choose yes, **VSConan** will generate a default configuration file in your workspace to start with. If you want to configure your workspace manually, we also provide you possibility to create a default configuration file using VS Code command `VSConan: Create Workspace Configuration (JSON)`. Currently supported conan command for configuration file: @@ -169,7 +169,7 @@ Currently supported conan command for configuration file: > The execution of the conan command will be done by the interpreter / conan executable from the profile that you selected. This configuration can be used for Conan version 1 and 2. -The default configuration file can be seen as following. You can extend the list of each command to have different name, description, user, channel and many other details. +The default configuration file can be seen as following. You can extend the list of each command to have different name, description, user, channel and many other details. ```json { @@ -257,11 +257,30 @@ 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)` +* `VSConan: Create Workspace Configuration (JSON)` Create workspace configuration file -* `VSConan: Open Workspace Configuration (JSON)` +* `VSConan: Open Workspace Configuration (JSON)` Open the workspace configuration file in the editor Further information of current supported features is available [here](doc/FEATURES.md). diff --git a/doc/FEATURES.md b/doc/FEATURES.md index 039557e..e195735 100644 --- a/doc/FEATURES.md +++ b/doc/FEATURES.md @@ -18,7 +18,7 @@ * Remove recipe from local cache * Remove editable package recipe from editable mode * Filter list of recipe based on a remote - + ### Conan - Package * Show list of binary packages from a recipe * Open in Explorer @@ -41,7 +41,7 @@ * Edit profile in VS Code editor * Open in Explorer * Rename profile -* Duplicate profile +* Duplicate profile * Remove profile ### Conan - Remote @@ -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. @@ -87,4 +88,4 @@ } ``` * Overwrite conan home folder inside the profile with `conanUserHome` -* Status bar to ease the switching between your predefined profiles \ No newline at end of file +* Status bar to ease the switching between your predefined profiles 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"]); + }); +});