Skip to content

Commit

Permalink
Support Miniconda on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
jwodder committed Feb 3, 2021
1 parent 877d085 commit 6fe20af
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 50 deletions.
13 changes: 11 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ on:
schedule:
- cron: '0 6 * * *'

defaults:
run:
shell: bash

jobs:
test:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- windows-latest
- ubuntu-latest
python-version:
- '3.6'
- '3.7'
Expand All @@ -22,6 +30,7 @@ jobs:
include:
- python-version: '3.6'
toxenv: typing
os: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v2
Expand All @@ -38,7 +47,7 @@ jobs:
- name: Run tests
if: matrix.toxenv == 'py'
run: tox -e py -- --cov-report=xml
run: ./run-tests.sh -e py -- --cov-report=xml

- name: Run generic tests
if: matrix.toxenv != 'py'
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
include CHANGELOG.* CONTRIBUTORS.* LICENSE tox.ini
include CHANGELOG.* CONTRIBUTORS.* LICENSE run-tests.sh tox.ini
graft src
graft docs
prune docs/_build
Expand Down
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ Options
'''''''

--batch Run the Miniconda installation script in batch
(noninteractive) mode.
(noninteractive) mode. This option is always
in effect when installing on Windows.

-e ARGS, --extra-args ARGS Specify extra command-line arguments to pass to
the Miniconda installation script.
Expand Down
12 changes: 12 additions & 0 deletions run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
if [ "$(uname)" = Darwin ]
then
# The lengthy default $TMPDIR on macOS causes lengthy shebangs when
# installing Miniconda. If the shebang exceeds 127 characters, Miniconda
# refuses to use it, instead setting the first line of the "conda" script
# to "#!/usr/bin/env python", which results in a non-working installation.
# Hence, we need a shorter $TMPDIR.
export TMPDIR=/tmp
fi

exec tox "$@"
62 changes: 48 additions & 14 deletions src/datalad_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,26 @@ class CondaInstance(NamedTuple):
#: The name of the environment (`None` for the base environment)
name: Optional[str]

@property
def conda_exe(self) -> Path:
""" The path to the Conda executable """
if platform.system() == "Windows":
return self.basepath / "Scripts" / "conda.exe"
else:
return self.basepath / "bin" / "conda"

@property
def bindir(self) -> Path:
"""
The directory in which command-line programs provided by packages are
installed
"""
dirname = "Scripts" if platform.system() == "Windows" else "bin"
if self.name is None:
return self.basepath / dirname
else:
return self.basepath / "envs" / self.name / dirname


#: A list of command names and the paths at which they are located
CommandList = List[Tuple[str, Path]]
Expand Down Expand Up @@ -779,6 +799,7 @@ def provide(
extra_args: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
systype = platform.system()
log.info("Installing Miniconda")
if path is None:
path = mktempdir("dl-miniconda-")
Expand All @@ -789,16 +810,20 @@ def provide(
# command line.)
path.rmdir()
log.info("Path: %s", path)
log.info("Batch: %s", batch)
if systype == "Windows":
log.info("Batch: True")
else:
log.info("Batch: %s", batch)
log.info("Spec: %s", spec)
log.info("Extra args: %s", extra_args)
if kwargs:
log.warning("Ignoring extra component arguments: %r", kwargs)
systype = platform.system()
if systype == "Linux":
miniconda_script = "Miniconda3-latest-Linux-x86_64.sh"
elif systype == "Darwin":
miniconda_script = "Miniconda3-latest-MacOSX-x86_64.sh"
elif systype == "Windows":
miniconda_script = "Miniconda3-latest-Windows-x86_64.exe"
else:
raise RuntimeError(f"E: Unsupported OS: {systype}")
log.info("Downloading and running miniconda installer")
Expand All @@ -814,12 +839,24 @@ def provide(
script_path,
)
log.info("Installing miniconda in %s", path)
args = ["-p", path, "-s"]
if batch:
args.append("-b")
if extra_args is not None:
args.extend(extra_args)
runcmd("bash", script_path, *args)
if systype == "Windows":
# `path` needs to be absolute when passing it to the installer,
# but Path.resolve() is a no-op for non-existent files on
# Windows. Hence, we need to create the directory first.
path.mkdir(parents=True, exist_ok=True)
cmd = f'start /wait "" {script_path}'
if extra_args is not None:
cmd += " ".join(extra_args)
cmd += f" /S /D={path.resolve()}"
log.info("Running: %s", cmd)
subprocess.run(cmd, check=True, shell=True)
else:
args = ["-p", path, "-s"]
if batch:
args.append("-b")
if extra_args is not None:
args.extend(extra_args)
runcmd("bash", script_path, *args)
if spec is not None:
runcmd(path / "bin" / "conda", "install", *spec)
conda_instance = CondaInstance(basepath=path, name=None)
Expand Down Expand Up @@ -879,7 +916,7 @@ def provide(
if kwargs:
log.warning("Ignoring extra component arguments: %r", kwargs)
conda = self.manager.get_conda()
cmd = [conda.basepath / "bin" / "conda", "create", "--name", cname]
cmd = [conda.conda_exe, "create", "--name", cname]
if extra_args is not None:
cmd.extend(extra_args)
if spec is not None:
Expand Down Expand Up @@ -1435,7 +1472,7 @@ def install_package(
log.info("Extra args: %s", extra_args)
if kwargs:
log.warning("Ignoring extra installer arguments: %r", kwargs)
cmd = [conda.basepath / "bin" / "conda", "install"]
cmd = [conda.conda_exe, "install"]
if conda.name is not None:
cmd.append("--name")
cmd.append(conda.name)
Expand All @@ -1447,10 +1484,7 @@ def install_package(
else:
cmd.append(f"{package}={version}")
runcmd(*cmd)
if conda.name is None:
binpath = conda.basepath / "bin"
else:
binpath = conda.basepath / "envs" / conda.name / "bin"
binpath = conda.bindir
log.debug("Installed program directory: %s", binpath)
return binpath

Expand Down
59 changes: 34 additions & 25 deletions test/test_install.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import logging
from pathlib import Path
import platform
import subprocess
import tempfile
import pytest
from datalad_installer import main


def bin_path(binname):
if platform.system() == "Windows":
return Path("Scripts", binname + ".exe")
else:
return Path("bin", binname)


@pytest.fixture(autouse=True)
def capture_all_logs(caplog):
caplog.set_level(logging.DEBUG)
Expand All @@ -22,9 +31,9 @@ def test_install_miniconda(tmp_path):
]
)
assert r == 0
assert (miniconda_path / "bin" / "conda").exists()
assert (miniconda_path / bin_path("conda")).exists()
r = subprocess.run(
[str(miniconda_path / "bin" / "conda"), "create", "-n", "test", "-y"],
[str(miniconda_path / bin_path("conda")), "create", "-n", "test", "-y"],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
Expand All @@ -46,9 +55,9 @@ def test_install_miniconda_autogen_path(monkeypatch, tmp_path):
(miniconda_path,) = [
p for p in tmp_path.iterdir() if p.name.startswith("dl-miniconda-")
]
assert (miniconda_path / "bin" / "conda").exists()
assert (miniconda_path / bin_path("conda")).exists()
r = subprocess.run(
[str(miniconda_path / "bin" / "conda"), "create", "-n", "test", "-y"],
[str(miniconda_path / bin_path("conda")), "create", "-n", "test", "-y"],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
Expand All @@ -70,8 +79,8 @@ def test_install_miniconda_datalad(tmp_path):
]
)
assert r == 0
assert (miniconda_path / "bin" / "conda").exists()
assert (miniconda_path / "bin" / "datalad").exists()
assert (miniconda_path / bin_path("conda")).exists()
assert (miniconda_path / bin_path("datalad")).exists()


def test_install_miniconda_conda_env_datalad(tmp_path):
Expand All @@ -90,10 +99,10 @@ def test_install_miniconda_conda_env_datalad(tmp_path):
]
)
assert r == 0
assert (miniconda_path / "bin" / "conda").exists()
assert not (miniconda_path / "bin" / "datalad").exists()
assert (miniconda_path / bin_path("conda")).exists()
assert not (miniconda_path / bin_path("datalad")).exists()
assert (miniconda_path / "envs" / "foo").exists()
assert (miniconda_path / "envs" / "foo" / "bin" / "datalad").exists()
assert (miniconda_path / "envs" / "foo" / bin_path("datalad")).exists()


def test_install_venv_miniconda_datalad(tmp_path):
Expand All @@ -113,10 +122,10 @@ def test_install_venv_miniconda_datalad(tmp_path):
]
)
assert r == 0
assert (venv_path / "bin" / "python").exists()
assert not (venv_path / "bin" / "datalad").exists()
assert (miniconda_path / "bin" / "conda").exists()
assert (miniconda_path / "bin" / "datalad").exists()
assert (venv_path / bin_path("python")).exists()
assert not (venv_path / bin_path("datalad")).exists()
assert (miniconda_path / bin_path("conda")).exists()
assert (miniconda_path / bin_path("datalad")).exists()


def test_install_venv_miniconda_conda_env_datalad(tmp_path):
Expand All @@ -139,12 +148,12 @@ def test_install_venv_miniconda_conda_env_datalad(tmp_path):
]
)
assert r == 0
assert (venv_path / "bin" / "python").exists()
assert not (venv_path / "bin" / "datalad").exists()
assert (miniconda_path / "bin" / "conda").exists()
assert not (miniconda_path / "bin" / "datalad").exists()
assert (venv_path / bin_path("python")).exists()
assert not (venv_path / bin_path("datalad")).exists()
assert (miniconda_path / bin_path("conda")).exists()
assert not (miniconda_path / bin_path("datalad")).exists()
assert (miniconda_path / "envs" / "foo").exists()
assert (miniconda_path / "envs" / "foo" / "bin" / "datalad").exists()
assert (miniconda_path / "envs" / "foo" / bin_path("datalad")).exists()


def test_install_venv_datalad(tmp_path):
Expand All @@ -159,8 +168,8 @@ def test_install_venv_datalad(tmp_path):
]
)
assert r == 0
assert (venv_path / "bin" / "python").exists()
assert (venv_path / "bin" / "datalad").exists()
assert (venv_path / bin_path("python")).exists()
assert (venv_path / bin_path("datalad")).exists()


def test_install_miniconda_conda_env_venv_datalad(tmp_path):
Expand All @@ -183,9 +192,9 @@ def test_install_miniconda_conda_env_venv_datalad(tmp_path):
]
)
assert r == 0
assert (venv_path / "bin" / "python").exists()
assert (venv_path / "bin" / "datalad").exists()
assert (miniconda_path / "bin" / "conda").exists()
assert not (miniconda_path / "bin" / "datalad").exists()
assert (venv_path / bin_path("python")).exists()
assert (venv_path / bin_path("datalad")).exists()
assert (miniconda_path / bin_path("conda")).exists()
assert not (miniconda_path / bin_path("datalad")).exists()
assert (miniconda_path / "envs" / "foo").exists()
assert not (miniconda_path / "envs" / "foo" / "bin" / "datalad").exists()
assert not (miniconda_path / "envs" / "foo" / bin_path("datalad")).exists()
7 changes: 0 additions & 7 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ isolated_build = True
minversion = 3.3.0

[testenv]
setenv =
# The lengthy default $TMPDIR on macOS causes lengthy shebangs when
# installing Miniconda. If the shebang exceeds 127 characters, Miniconda
# refuses to use it, instead setting the first line of the "conda" script
# to "#!/usr/bin/env python", which results in a non-working installation.
# Hence, we need a shorter $TMPDIR.
TMPDIR=/tmp
deps =
flake8~=3.7
flake8-bugbear
Expand Down

0 comments on commit 6fe20af

Please sign in to comment.