Skip to content

Commit ad43992

Browse files
Merge pull request #45 from abrahammurciano/link-conflict
2 parents d66ed74 + 66920db commit ad43992

File tree

3 files changed

+119
-33
lines changed

3 files changed

+119
-33
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/core.py

+79-24
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,92 @@
22
import pathlib
33
import subprocess
44
import sys
5+
from enum import Enum
56

67
from . import conda
78
from .config import CONDA_ENV_PREFIX_PATH, CONDAX_LINK_DESTINATION, DEFAULT_CHANNELS
89
from .paths import mkpath
910

1011

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

75+
def create_link(exe, link_conflict_action):
76+
create_link_func = create_link_windows if sys.platform == "nt" else create_link_unix
77+
return create_link_func(exe, link_conflict_action)
2978

30-
def create_links(executables_to_link):
79+
80+
def create_links(executables_to_link, link_conflict_action):
81+
print(os.listdir(CONDAX_LINK_DESTINATION))
82+
link_succeeded = {}
3183
for exe in executables_to_link:
32-
create_link(exe)
84+
link_succeeded[exe] = create_link(exe, link_conflict_action)
3385
if len(executables_to_link):
3486
print("Created the following entrypoint links:", file=sys.stderr)
3587
for exe in executables_to_link:
36-
executable_name = os.path.basename(exe)
37-
print(f" {executable_name}", file=sys.stderr)
88+
if link_succeeded[exe]:
89+
executable_name = os.path.basename(exe)
90+
print(f" {executable_name}", file=sys.stderr)
3891

3992

4093
def remove_links(executables_to_unlink):
@@ -50,11 +103,13 @@ def remove_links(executables_to_unlink):
50103
print(f" {executable_name}", file=sys.stderr)
51104

52105

53-
def install_package(package, channels=DEFAULT_CHANNELS):
106+
def install_package(
107+
package, channels=DEFAULT_CHANNELS, link_conflict_action=LinkConflictAction.ERROR
108+
):
54109
conda.create_conda_environment(package, channels=channels)
55110
executables_to_link = conda.detemine_executables_from_env(package)
56111
mkpath(CONDAX_LINK_DESTINATION)
57-
create_links(executables_to_link)
112+
create_links(executables_to_link, link_conflict_action)
58113
print(f"`{package}` has been installed by condax", file=sys.stderr)
59114

60115

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

76131

77-
def update_all_packages():
132+
def update_all_packages(link_conflict_action=LinkConflictAction.ERROR):
78133
for package in os.listdir(CONDA_ENV_PREFIX_PATH):
79134
if os.path.isdir(os.path.join(CONDA_ENV_PREFIX_PATH, package)):
80-
update_package(package)
135+
update_package(package, link_conflict_action)
81136

82137

83-
def update_package(package):
138+
def update_package(package, link_conflict_action=LinkConflictAction.ERROR):
84139
exit_if_not_installed(package)
85140
try:
86141
executables_already_linked = set(conda.detemine_executables_from_env(package))
@@ -92,8 +147,8 @@ def update_package(package):
92147
to_create = executables_linked_in_updated - executables_already_linked
93148
to_delete = executables_already_linked - executables_linked_in_updated
94149

95-
create_links(to_create)
96150
remove_links(to_delete)
151+
create_links(to_create, link_conflict_action)
97152
print(f"{package} update successfully")
98153

99154
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)