Skip to content

Commit af95d82

Browse files
Abraham Murcianomariusvniekerk
Abraham Murciano
authored andcommitted
Added --link-conflict flag
With this flag the user can specify what to do when a link already exists in the ~/.local/bin directory. The default is `error`, which will cause the install to fail. The other options are `skip` and `overwrite`.
1 parent d8cb705 commit af95d82

File tree

4 files changed

+110
-34
lines changed

4 files changed

+110
-34
lines changed

condax/cli.py

+35-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def cli():
1313
@cli.command(
1414
help=f"""
1515
Install a package with condax.
16-
16+
1717
This will install a package into a new conda environment and link the executable
1818
provided by it to `{config.CONDAX_LINK_DESTINATION}`.
1919
"""
@@ -22,20 +22,35 @@ def cli():
2222
"--channel",
2323
"-c",
2424
multiple=True,
25-
help=f"""Use the channels specified to install. If not specified condax will
25+
help=f"""Use the channels specified to install. If not specified condax will
2626
default to using {config.DEFAULT_CHANNELS}.""",
2727
)
28+
@click.option(
29+
"--link-conflict",
30+
"-l",
31+
default="error",
32+
type=click.Choice([action.value for action in core.LinkConflictAction]),
33+
help=f"""How to handle conflicts when a link with the same name already exists in
34+
`{config.CONDAX_LINK_DESTINATION}`. If `error` is specified, condax will exit with
35+
an error if the link already exists. If `overwrite` is specified, condax will
36+
overwrite the existing link. If `skip` is specified, condax will skip linking the
37+
conflicting executable.""",
38+
)
2839
@click.argument("package")
29-
def install(channel, package):
40+
def install(channel, package, link_conflict):
3041
if channel is None or (len(channel) == 0):
3142
channel = config.DEFAULT_CHANNELS
32-
core.install_package(package, channels=channel)
43+
core.install_package(
44+
package,
45+
channels=channel,
46+
link_conflict_action=core.LinkConflictAction(link_conflict)
47+
)
3348

3449

3550
@cli.command(
3651
help="""
3752
Remove a package installed by condax.
38-
53+
3954
This will remove a package installed with condax and destroy the underlying
4055
conda environment.
4156
"""
@@ -58,20 +73,31 @@ def ensure_path():
5873
@cli.command(
5974
help="""
6075
Update package(s) installed by condax.
61-
76+
6277
This will update the underlying conda environments(s) to the latest release of a package.
6378
"""
6479
)
6580
@click.option(
6681
"--all", is_flag=True, help="Set to update all packages installed by condax"
6782
)
83+
@click.option(
84+
"--link-conflict",
85+
"-l",
86+
default="error",
87+
type=click.Choice([action.value for action in core.LinkConflictAction]),
88+
help=f"""How to handle conflicts when a link with the same name already exists in
89+
`{config.CONDAX_LINK_DESTINATION}`. If `error` is specified, condax will exit with
90+
an error if the link already exists. If `overwrite` is specified, condax will
91+
overwrite the existing link. If `skip` is specified, condax will skip linking the
92+
conflicting executable.""",
93+
)
6894
@click.argument("package", default="", required=False)
6995
@click.pass_context
70-
def update(ctx, all, package):
96+
def update(ctx, all, link_conflict, package):
7197
if all:
72-
core.update_all_packages()
98+
core.update_all_packages(core.LinkConflictAction(link_conflict))
7399
elif package:
74-
core.update_package(package)
100+
core.update_package(package, core.LinkConflictAction(link_conflict))
75101
else:
76102
print(ctx.get_help(), file=sys.stderr)
77103

condax/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818

1919
CONDA_ENV_PREFIX_PATH = os.path.expanduser(_config["prefix_path"])
2020
CONDAX_LINK_DESTINATION = os.path.expanduser(_config["link_destination"])
21-
DEFAULT_CHANNELS = _config["channels"]
21+
DEFAULT_CHANNELS = _config["channels"]

condax/core.py

+69-24
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
import os
23
import pathlib
34
import subprocess
@@ -7,34 +8,78 @@
78
from .config import CONDA_ENV_PREFIX_PATH, CONDAX_LINK_DESTINATION, DEFAULT_CHANNELS
89
from .paths import mkpath
910

11+
class LinkConflictAction(Enum):
12+
ERROR = "error"
13+
OVERWRITE = "overwrite"
14+
SKIP = "skip"
1015

11-
def create_link(exe):
16+
17+
def create_link_windows(exe, link_conflict_action):
1218
executable_name = os.path.basename(exe)
13-
if os.name == "nt":
14-
# create a batch file to run our application
15-
win_path = pathlib.PureWindowsPath(exe)
16-
name_only, _ = os.path.splitext(executable_name)
17-
with open(f"{CONDAX_LINK_DESTINATION}/{name_only}.bat", "w") as fo:
18-
fo.writelines(
19-
[
20-
"@echo off\n",
21-
"REM Entrypoint created by condax\n",
22-
f'CALL "{win_path}" %*',
23-
]
19+
# create a batch file to run our application
20+
win_path = pathlib.PureWindowsPath(exe)
21+
name_only, _ = os.path.splitext(executable_name)
22+
bat_path = f"{CONDAX_LINK_DESTINATION}/{name_only}.bat"
23+
if os.path.exists(bat_path):
24+
if link_conflict_action == LinkConflictAction.ERROR:
25+
print(
26+
f"Error: link already exists for {executable_name}, use --link-conflict to overwrite or skip",
27+
file=sys.stderr,
2428
)
25-
else:
26-
print(os.listdir(CONDAX_LINK_DESTINATION))
29+
sys.exit(1)
30+
elif link_conflict_action == LinkConflictAction.SKIP:
31+
print(f"Skipping link for {executable_name} because it already exists", file=sys.stderr)
32+
return False
33+
elif link_conflict_action == LinkConflictAction.OVERWRITE:
34+
print(f"Overwriting existing link {bat_path}", file=sys.stderr)
35+
os.remove(bat_path)
36+
with open(bat_path, "w") as fo:
37+
fo.writelines(
38+
[
39+
"@echo off\n",
40+
"REM Entrypoint created by condax\n",
41+
f'CALL "{win_path}" %*',
42+
]
43+
)
44+
return True
45+
46+
47+
def create_link_unix(exe, link_conflict_action):
48+
executable_name = os.path.basename(exe)
49+
dst = f"{CONDAX_LINK_DESTINATION}/{executable_name}"
50+
if link_conflict_action == LinkConflictAction.OVERWRITE and os.path.exists(dst):
51+
print(f"Overwriting existing link {dst}", file=sys.stderr)
52+
os.remove(dst)
53+
try:
2754
os.symlink(exe, f"{CONDAX_LINK_DESTINATION}/{executable_name}")
55+
return True
56+
except FileExistsError:
57+
if link_conflict_action == LinkConflictAction.ERROR:
58+
print(
59+
f"Error: link already exists for {executable_name}, use --link-conflict to overwrite or skip",
60+
file=sys.stderr,
61+
)
62+
sys.exit(1)
63+
elif link_conflict_action == LinkConflictAction.SKIP:
64+
print(f"Skipping link for {executable_name} because it already exists", file=sys.stderr)
65+
return False
66+
2867

68+
def create_link(exe, link_conflict_action):
69+
create_link_func = create_link_windows if sys.platform == "nt" else create_link_unix
70+
return create_link_func(exe, link_conflict_action)
2971

30-
def create_links(executables_to_link):
72+
def create_links(executables_to_link, link_conflict_action):
73+
print(os.listdir(CONDAX_LINK_DESTINATION))
74+
link_succeeded = {}
3175
for exe in executables_to_link:
32-
create_link(exe)
76+
link_succeeded[exe] = create_link(exe, link_conflict_action)
3377
if len(executables_to_link):
3478
print("Created the following entrypoint links:", file=sys.stderr)
3579
for exe in executables_to_link:
36-
executable_name = os.path.basename(exe)
37-
print(f" {executable_name}", file=sys.stderr)
80+
if link_succeeded[exe]:
81+
executable_name = os.path.basename(exe)
82+
print(f" {executable_name}", file=sys.stderr)
3883

3984

4085
def remove_links(executables_to_unlink):
@@ -50,11 +95,11 @@ def remove_links(executables_to_unlink):
5095
print(f" {executable_name}", file=sys.stderr)
5196

5297

53-
def install_package(package, channels=DEFAULT_CHANNELS):
98+
def install_package(package, channels=DEFAULT_CHANNELS, link_conflict_action=LinkConflictAction.ERROR):
5499
conda.create_conda_environment(package, channels=channels)
55100
executables_to_link = conda.detemine_executables_from_env(package)
56101
mkpath(CONDAX_LINK_DESTINATION)
57-
create_links(executables_to_link)
102+
create_links(executables_to_link, link_conflict_action)
58103
print(f"`{package}` has been installed by condax", file=sys.stderr)
59104

60105

@@ -74,13 +119,13 @@ def remove_package(package):
74119
print(f"`{package}` has been removed from condax", file=sys.stderr)
75120

76121

77-
def update_all_packages():
122+
def update_all_packages(link_conflict_action=LinkConflictAction.ERROR):
78123
for package in os.listdir(CONDA_ENV_PREFIX_PATH):
79124
if os.path.isdir(os.path.join(CONDA_ENV_PREFIX_PATH, package)):
80-
update_package(package)
125+
update_package(package, link_conflict_action)
81126

82127

83-
def update_package(package):
128+
def update_package(package, link_conflict_action=LinkConflictAction.ERROR):
84129
exit_if_not_installed(package)
85130
try:
86131
executables_already_linked = set(conda.detemine_executables_from_env(package))
@@ -93,7 +138,7 @@ def update_package(package):
93138
to_create = executables_already_linked - executables_linked_in_updated
94139

95140
remove_links(to_delete)
96-
create_links(to_create)
141+
create_links(to_create, link_conflict_action)
97142
print(f"{package} update successfully")
98143

99144
except subprocess.CalledProcessError:

news/link-conflict.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Added:
2+
3+
* `--link-conflict` flag to specify what to do when a link already exists in the
4+
~/.local/bin directory. The default is `error`, which will cause the install to
5+
fail. The other options are `skip` and `overwrite`.

0 commit comments

Comments
 (0)