-
Notifications
You must be signed in to change notification settings - Fork 992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Fish support for environments #15503
Changes from 10 commits
5f19714
bf5d21e
b249c6d
e16bf22
d42b388
269b4c8
e703b07
88a48b4
c60eb5a
1ab3a8d
70c34ae
37eab05
e32b420
8cdfd29
966c631
dd517c6
325fe06
2d202ed
66ca6a2
f57f32f
4535b74
32125de
cd94ce3
35d682e
d55eff5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,12 @@ | ||
import os | ||
import textwrap | ||
import random | ||
import string | ||
from jinja2 import Template | ||
from collections import OrderedDict | ||
from contextlib import contextmanager | ||
from pathlib import Path | ||
|
||
|
||
from conans.client.generators import relativize_paths | ||
from conans.client.subsystems import deduce_subsystem, WINDOWS, subsystem_path | ||
|
@@ -19,16 +24,19 @@ def environment_wrap_command(env_filenames, env_folder, cmd, subsystem=None, | |
if not env_filenames: | ||
return cmd | ||
filenames = [env_filenames] if not isinstance(env_filenames, list) else env_filenames | ||
bats, shs, ps1s = [], [], [] | ||
bats, shs, ps1s, fishs = [], [], [], [] | ||
|
||
accept = accepted_extensions or ("ps1", "bat", "sh") | ||
# TODO: This implemantation is dirty, improve it | ||
accept = accepted_extensions or ("ps1", "bat", "sh", "fish") | ||
# TODO: This implementation is dirty, improve it | ||
for f in filenames: | ||
f = f if os.path.isabs(f) else os.path.join(env_folder, f) | ||
if f.lower().endswith(".sh"): | ||
if os.path.isfile(f) and "sh" in accept: | ||
f = subsystem_path(subsystem, f) | ||
shs.append(f) | ||
elif f.lower().endswith(".fish"): | ||
if os.path.isfile(f) and "fish" in accept: | ||
fishs.append(f) | ||
elif f.lower().endswith(".bat"): | ||
if os.path.isfile(f) and "bat" in accept: | ||
bats.append(f) | ||
|
@@ -39,17 +47,20 @@ def environment_wrap_command(env_filenames, env_folder, cmd, subsystem=None, | |
path_bat = "{}.bat".format(f) | ||
path_sh = "{}.sh".format(f) | ||
path_ps1 = "{}.ps1".format(f) | ||
path_fish = "{}.fish".format(f) | ||
if os.path.isfile(path_bat) and "bat" in accept: | ||
bats.append(path_bat) | ||
if os.path.isfile(path_ps1) and "ps1" in accept: | ||
ps1s.append(path_ps1) | ||
if os.path.isfile(path_sh) and "sh" in accept: | ||
path_sh = subsystem_path(subsystem, path_sh) | ||
shs.append(path_sh) | ||
if os.path.isfile(path_fish) and "fish" in accept: | ||
fishs.append(path_fish) | ||
|
||
if bool(bats + ps1s) + bool(shs) > 1: | ||
if bool(bats + ps1s) + bool(shs) > 1 + bool(fishs) > 1: | ||
raise ConanException("Cannot wrap command with different envs," | ||
"{} - {}".format(bats+ps1s, shs)) | ||
"{} - {} - {}".format(bats+ps1s, shs, fishs)) | ||
|
||
if bats: | ||
launchers = " && ".join('"{}"'.format(b) for b in bats) | ||
|
@@ -62,6 +73,9 @@ def environment_wrap_command(env_filenames, env_folder, cmd, subsystem=None, | |
elif shs: | ||
launchers = " && ".join('. "{}"'.format(f) for f in shs) | ||
return '{} && {}'.format(launchers, cmd) | ||
elif fishs: | ||
launchers = " && ".join('. "{}"'.format(f) for f in fishs) | ||
return 'fish -c "{}; and {}"'.format(launchers, cmd) | ||
elif ps1s: | ||
# TODO: at the moment it only works with path without spaces | ||
launchers = " ; ".join('"&\'{}\'"'.format(f) for f in ps1s) | ||
|
@@ -520,12 +534,66 @@ def save_sh(self, file_location, generate_deactivate=True): | |
content = f'script_folder="{os.path.abspath(filepath)}"\n' + content | ||
save(file_location, content) | ||
|
||
def save_fish(self, file_location): | ||
"""Save a fish script file with the environment variables defined in the Environment object. | ||
|
||
It generates a function to deactivate the environment variables configured in the Environment object. | ||
|
||
:param file_location: The path to the file to save the fish script. | ||
""" | ||
filepath, filename = os.path.split(file_location) | ||
function_name = f"deactivate_{Path(filename).stem}" | ||
group = "".join(random.choices(string.ascii_letters + string.digits, k=8)) | ||
script_content = Template(textwrap.dedent(""" | ||
function {{ function_name }} | ||
echo "echo Restoring environment" | ||
for var in $conan_{{ group }}_del | ||
set -e $var | ||
end | ||
set -e $conan_{{ group }}_del | ||
{% for item in vars_define.keys() %} | ||
if set -q conan_{{ group }}_{{ item }} | ||
set -gx {{ item }} "$conan_{{ group }}_{{ item }}" | ||
end | ||
{% endfor %} | ||
end | ||
{% for item, value in vars_define.items() %} | ||
{% if item == "PATH" %} | ||
set -g conan_{{ group }}_{{ item }} "${{ item }}" | ||
fish_add_path -pg "{{ value }}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are other variables that are paths too, like PYTHONPATH, and every other var marked as "path", shouldn't they do the same? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately I didn't find a similar feature for custom paths, but I'll check with Rubén tomorrow for insights. |
||
{% else %} | ||
if not set -q {{ item }} | ||
set -ga conan_{{ group }}_del {{ item }} | ||
set -gx {{ item }} "{{ value }}" | ||
else | ||
set -g conan_{{ group }}_{{ item }} "${{ item }}" | ||
set -pgx {{ item }} "{{ value }}" | ||
end | ||
{% endif %} | ||
{% endfor %} | ||
""")) | ||
values = self._values.keys() | ||
vars_define = {} | ||
for varname, varvalues in self._values.items(): | ||
abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder") | ||
value = varvalues.get_str("", self._subsystem, pathsep=self._pathsep, | ||
root_path=abs_base_path, script_path=new_path) | ||
value = value.replace('"', '\\"') | ||
vars_define[varname] = value | ||
|
||
if values: | ||
content = script_content.render(function_name=function_name, group=group, | ||
vars_define=vars_define) | ||
save(file_location, content) | ||
|
||
def save_script(self, filename): | ||
""" | ||
Saves a script file (bat, sh, ps1) with a launcher to set the environment. | ||
If the conf "tools.env.virtualenv:powershell" is set to True it will generate powershell | ||
launchers if Windows. | ||
|
||
If the conf "tools.env.virtualenv:fish" is set to True it will generate fish launchers. | ||
|
||
:param filename: Name of the file to generate. If the extension is provided, it will generate | ||
the launcher script for that extension, otherwise the format will be deduced | ||
checking if we are running inside Windows (checking also the subsystem) or not. | ||
|
@@ -534,18 +602,27 @@ def save_script(self, filename): | |
if ext: | ||
is_bat = ext == ".bat" | ||
is_ps1 = ext == ".ps1" | ||
is_fish = ext == ".fish" | ||
else: # Need to deduce it automatically | ||
is_bat = self._subsystem == WINDOWS | ||
is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=bool) | ||
if is_ps1: | ||
is_fish = self._conanfile.conf.get("tools.env.virtualenv:fish", check_type=bool) | ||
memsharded marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if is_fish: | ||
filename = filename + ".fish" | ||
is_bat = False | ||
is_ps1 = False | ||
elif is_ps1: | ||
filename = filename + ".ps1" | ||
is_bat = False | ||
is_fish = False | ||
else: | ||
filename = filename + (".bat" if is_bat else ".sh") | ||
|
||
path = os.path.join(self._conanfile.generators_folder, filename) | ||
if is_bat: | ||
self.save_bat(path) | ||
elif is_fish: | ||
self.save_fish(path) | ||
elif is_ps1: | ||
self.save_ps1(path) | ||
else: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import os | ||
import platform | ||
|
||
import pytest | ||
|
||
from conans.test.assets.genconanfile import GenConanfile | ||
from conans.test.utils.test_files import temp_folder | ||
from conans.test.utils.tools import TestClient | ||
from conans.util.files import save | ||
|
||
|
||
@pytest.mark.tool("fish") | ||
def test_define_new_vars(): | ||
"""Test when defining new path and new variable in buildenv_info. | ||
|
||
Variables should be available as environment variables in the buildenv script. | ||
And, should be deleted when deactivate_conanbuildenv is called. | ||
""" | ||
cache_folder = os.path.join(temp_folder(), "[sub] folder") | ||
client = TestClient(cache_folder) | ||
conanfile = str(GenConanfile("pkg", "0.1")) | ||
conanfile += """ | ||
|
||
def package_info(self): | ||
self.buildenv_info.define_path("MYPATH1", "/path/to/ar") | ||
self.buildenv_info.define("MYVAR1", "myvalue") | ||
self.buildenv_info.prepend_path("PATH", "/path/to/fake/folder") | ||
""" | ||
client.save({"conanfile.py": conanfile}) | ||
client.run("create .") | ||
save(client.cache.new_config_path, "tools.env.virtualenv:fish=True\n") | ||
client.save({"conanfile.py": GenConanfile("app", "0.1").with_requires("pkg/0.1")}) | ||
client.run("install . -s:a os=Linux") | ||
|
||
assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.sh")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.bat")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.sh")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.bat")) | ||
|
||
assert os.path.exists(os.path.join(client.current_folder, "conanbuildenv.fish")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.fish")) | ||
|
||
with open(os.path.join(client.current_folder, "conanbuildenv.fish"), "r") as f: | ||
buildenv = f.read() | ||
assert 'set -gx MYPATH1 "/path/to/ar"' in buildenv | ||
assert 'set -gx MYVAR1 "myvalue"' in buildenv | ||
|
||
client.run_command("fish -c 'source conanbuildenv.fish && set'") | ||
assert 'MYPATH1 /path/to/ar' in client.out | ||
assert 'MYVAR1 myvalue' in client.out | ||
|
||
client.run_command("fish -c 'source conanbuildenv.fish && set && deactivate_conanbuildenv && set'") | ||
assert str(client.out).count('MYPATH1 /path/to/ar') == 1 | ||
uilianries marked this conversation as resolved.
Show resolved
Hide resolved
|
||
assert str(client.out).count('MYVAR1 myvalue') == 1 | ||
|
||
|
||
@pytest.mark.tool("fish") | ||
def test_append_path(): | ||
"""Test when appending to an existing path in buildenv_info. | ||
|
||
Path should be available as environment variable in the buildenv script, including the new value | ||
as first element. | ||
Once deactivate_conanbuildenv is called, the path should be restored as before. | ||
""" | ||
fake_path = "/path/to/fake/folder" | ||
cache_folder = os.path.join(temp_folder(), "[sub] folder") | ||
client = TestClient(cache_folder) | ||
conanfile = str(GenConanfile("pkg", "0.1")) | ||
conanfile += f""" | ||
|
||
def package_info(self): | ||
self.buildenv_info.prepend_path("PATH", "{fake_path}") | ||
""" | ||
client.save({"conanfile.py": conanfile}) | ||
current_path = os.environ["PATH"] | ||
client.run("create .") | ||
save(client.cache.new_config_path, "tools.env.virtualenv:fish=True\n") | ||
client.save({"conanfile.py": GenConanfile("app", "0.1").with_requires("pkg/0.1")}) | ||
client.run("install . -s:a os=Linux") | ||
|
||
assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.sh")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.bat")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.sh")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.bat")) | ||
|
||
assert os.path.exists(os.path.join(client.current_folder, "conanbuildenv.fish")) | ||
assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.fish")) | ||
|
||
with open(os.path.join(client.current_folder, "conanbuildenv.fish"), "r") as f: | ||
buildenv = f.read() | ||
assert f'set -pgx PATH "{fake_path}"' in buildenv | ||
|
||
client.run_command("fish -c 'source conanbuildenv.fish and set'") | ||
assert f'PATH {fake_path}' in client.out | ||
|
||
client.run_command("deactivate_conanbuildenv && set'") | ||
assert str(client.out).count(f'MYPATH1 {fake_path}') == 1 | ||
|
||
|
||
@pytest.mark.tool("fish") | ||
# TODO Pass variable with spaces | ||
@pytest.mark.parametrize("fish_value", [True, False]) | ||
@pytest.mark.parametrize("path_with_spaces", [True, False]) | ||
@pytest.mark.parametrize("value", ["Dulcinea del Toboso", "Dulcinea-Del-Toboso"]) | ||
def test_transitive_tool_requires(fish_value, path_with_spaces, value): | ||
"""Generate a tool require package, which provides and binary and a custom environment variable. | ||
Using fish, the binary should be available in the path, and the environment variable too. | ||
""" | ||
client = TestClient(path_with_spaces=path_with_spaces) | ||
save(client.cache.new_config_path, f"tools.env.virtualenv:fish={fish_value}\n") | ||
|
||
# Generate the tool package with pkg-echo-tool binary that prints the value of LADY env var | ||
cmd_line = "echo ${LADY}" if platform.system() != "Windows" else "echo %LADY%" | ||
conanfile = str(GenConanfile("tool", "0.1.0") | ||
.with_package_file("bin/pkg-echo-tool", cmd_line)) | ||
package_info = f""" | ||
os.chmod(os.path.join(self.package_folder, "bin", "pkg-echo-tool"), 0o777) | ||
|
||
def package_info(self): | ||
self.buildenv_info.define("LADY", "{value}") | ||
""" | ||
conanfile += package_info | ||
client.save({"tool/conanfile.py": conanfile}) | ||
client.run("create tool") | ||
|
||
assert "tool/0.1.0: package(): Packaged 1 file: pkg-echo-tool" in client.out | ||
|
||
# Generate the app package that uses the tool package. It should be able to run the binary and | ||
# access the environment variable as well. | ||
conanfile = str(GenConanfile("app", "0.1.0") | ||
.with_tool_requires("tool/0.1.0") | ||
.with_generator("VirtualBuildEnv")) | ||
build = """ | ||
def build(self): | ||
self.run("pkg-echo-tool", env="conanbuild") | ||
""" | ||
conanfile += build | ||
|
||
client.save({"app/conanfile.py": conanfile}) | ||
client.run("create app") | ||
|
||
assert value in client.out | ||
|
||
# TODO: Check why is generating when running Conan create: | ||
# - deactivate_conanbuildenv-release-armv8.fish | ||
# - conanrunenv-release-armv8.fish | ||
# No variables listed, only script_folder. Should not generate these files. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the function_name is this one? Why not just the
group
with the random chars? Is there risk of collision ofdeactivate_xxxx
function name?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It follow the common name used by other virtualenv like:
conanbuild.sh
->deactivate_conanbuild.sh
. So you can actually can call from your terminal directly, like:In Fish, functions are exported as a command, so you can run directly.
About the random to avoid collision: In case we have multiple dependencies add bin folder to PATH, we will need to undo the configuration after deactivating it. Usually it would be store in a deactivate script, but here we are using a variable. Actually, the random is just an idea, but could be the build folder name too, because is kind unique name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, having something deterministic would make more sense, I probably still didn't understand the issue.