Skip to content
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

Windows support and exposing interpreters #2

Merged
merged 2 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,24 @@ pyenv_install(
)
```

## Windows

If you want to use ```rules_pyenv``` on Windows you should [enable symlinks](https://docs.bazel.build/versions/master/windows.html#enable-symlink-support)

## Using the interpreter in repository rules

The interpreters are exposed as files, so you can refer to them in a repository rule.

You can refer to the ```python2``` interpreter as ```@pyenv//:py2/python``` and to the ```python3``` interpreter as ```@pyenv//:py3/python```.

## Build problems

```pyenv``` is building Python from sources, so you need all things necessary to build Python. If you have any problems regarding this, you should see [this page](https://github.com/pyenv/pyenv/wiki/common-build-problems) first.

## Caveats

- This thing needs tests
- This thing needs build automation
- This thing hasn't been tried on Windows (yet); hence tests and build automation
- You will probably find bugs. No, you're not crazy, it's just not working right; open an issue (PRs are welcome too)
- Since the native Bazel rules only support CPython your use of `pyenv` is restricted to CPython versions 2 and 3. If
you want additional support for other Python impl
Expand Down
170 changes: 118 additions & 52 deletions pyenv/defs.bzl
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")

DEFAULT_REPOSITORY_NAME = "pyenv"
PY2_DEFAULT_DIR_NAME = "py2"
PY3_DEFAULT_DIR_NAME = "py3"
INTERPRETER_DEFAULT_NAME = "python"

CONFIGURATIONS = {
"windows": {
"pyenv_dir": "bin",
"executable": "pyenv.bat",
"versions_dir": "versions",
"options": ["-q"],
"interpreter_dir": "",
"interpreter": "python.exe"
},
"unix": {
"pyenv_dir": "bin",
"executable": "pyenv",
"versions_dir": "versions",
"options": [],
"interpreter_dir": "bin",
"interpreter": "python"
}
}

BUILD_FILE_CONTENT = """# This file was automatically generated by @dpu_rules_pyenv//pyenv:defs.bzl
package(default_visibility = ["//visibility:public"])

load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair")

# export interpreter so it can accessed as a regular file
exports_files([
"{py2_dir_link}/{interpreter}",
"{py3_dir_link}/{interpreter}"
])

py_runtime(
name = "python_{py2}_runtime",
files = glob(["versions/{py2}/**/*"], exclude_directories = 0),
interpreter = "versions/{py2}/bin/python",
interpreter = "versions/{py2}/{interpreter_path}",
python_version = "PY2"
)

py_runtime(
name = "python_{py3}_runtime",
files = glob(["versions/{py3}/**/*"], exclude_directories = 0),
interpreter = "versions/{py3}/bin/python",
interpreter = "versions/{py3}/{interpreter_path}",
python_version = "PY3"
)

Expand All @@ -32,27 +62,30 @@ toolchain(
)
"""

def _is_windows(repository_ctx):
"""Returns true when os is recognized as windows
Args:
repository_ctx: The repository rule context
"""
os_family = repository_ctx.os.name.lower()
return os_family.find("windows") != -1

def _get_config(repository_ctx):
return CONFIGURATIONS["windows"] if _is_windows(repository_ctx) else CONFIGURATIONS["unix"]

def _download_pyenv(repository_ctx):
"""Used to download a pyenv version.
Args:
repository_ctx: The repository rule context
"""

pyenv_version = repository_ctx.attr._pyenv_version
pyenv_repositories = repository_ctx.attr._pyenv_repositories
os_family = "unix" if repository_ctx.os.name.lower().find("windows") == -1 else "windows"
version_os = "%s-%s" % (pyenv_version, os_family)

if version_os in pyenv_repositories:
url, strip_prefix, sha256 = pyenv_repositories[version_os]
else:
fail("Unknown pyenv version %s" % pyenv_version)
pyenv_repo = repository_ctx.attr.pyenv_win_repo if _is_windows(repository_ctx) else repository_ctx.attr.pyenv_repo

repository_ctx.download_and_extract(
url = [url],
url = pyenv_repo["url"],
output = "./",
stripPrefix = strip_prefix,
sha256 = sha256,
stripPrefix = pyenv_repo["strip_prefix"],
sha256 = pyenv_repo["sha256"],
)

def _setup_pyenv(repository_ctx):
Expand All @@ -70,73 +103,106 @@ def _setup_pyenv(repository_ctx):
if not pyenv_root:
fail("Unable to find PYENV_ROOT")

repository_ctx.symlink(pyenv_path, "bin/pyenv")
repository_ctx.symlink("{pyenv_root}/versions".format(pyenv_root = pyenv_root), "versions")
pyenv_dir = _get_config(repository_ctx)["pyenv_dir"]
executable = _get_config(repository_ctx)["executable"]
versions_dir = _get_config(repository_ctx)["versions_dir"]

repository_ctx.symlink(pyenv_path,
"{}/{}".format(pyenv_dir, executable))
repository_ctx.symlink("{}/{}".format(pyenv_root, versions_dir),
versions_dir)

def _install_python(repository_ctx, version):
if not version.startswith("2") and not version.startswith("3"):
fail("pyenv_install currently only supports cpython major versions 2 and 3")

repository_ctx.report_progress("Installing Python %s" % version)
pyenv_root = repository_ctx.path("./bin/pyenv").realpath.dirname.dirname
res = repository_ctx.execute(["bin/pyenv", "install", "-s", version], environment = {"PYENV_ROOT": str(pyenv_root)})

pyenv_dir = _get_config(repository_ctx)["pyenv_dir"]
executable = _get_config(repository_ctx)["executable"]
options = _get_config(repository_ctx)["options"]

pyenv_path = "./{}/{}".format(pyenv_dir, executable)
pyenv_root = repository_ctx.path(pyenv_path).realpath.dirname.dirname
# install in current directory, so intepreter can be accessed by "versions/..."
res = repository_ctx.execute(["bin/{}".format(executable), "install"] + options + [version],
environment = {"PYENV_ROOT": str(pyenv_root)})

if res.return_code:
fail("pyenv failed to install version %s" % version + res.stdout + res.stderr)

def _setup_build_file(repository_ctx, py2, py3):
def _setup_links(repository_ctx, version, dir_name):
interpreter_dir = _get_config(repository_ctx)["interpreter_dir"]
versions_dir = _get_config(repository_ctx)["versions_dir"]
bindir = "{}/{}".format(versions_dir, version) + ("/{}".format(interpreter_dir) if interpreter_dir else "")

# create symlink to bin directory for easier access by users
repository_ctx.symlink(bindir, dir_name)

# on windows interpreter is 'python.exe', so create a symlink to it and name it 'python', so it can be accessed the same way on windows and unix
interpreter = _get_config(repository_ctx)["interpreter"]
if interpreter != INTERPRETER_DEFAULT_NAME:
repository_ctx.symlink("{}/{}".format(bindir, interpreter), "{}/{}".format(bindir, INTERPRETER_DEFAULT_NAME))

def _setup_build_files(repository_ctx, py2, py3, py2_dir, py3_dir):
interpreter_dir = _get_config(repository_ctx)["interpreter_dir"]
interpreter_path = "{}/{}".format(interpreter_dir, INTERPRETER_DEFAULT_NAME) if interpreter_dir else INTERPRETER_DEFAULT_NAME
repository_ctx.file(
"BUILD.bazel",
content = BUILD_FILE_CONTENT.format(py2 = py2, py3 = py3),
"BUILD",
content = BUILD_FILE_CONTENT.format(py2 = py2,
py3 = py3,
py2_dir_link = py2_dir,
py3_dir_link = py3_dir,
interpreter = INTERPRETER_DEFAULT_NAME,
interpreter_path = interpreter_path)
)

def _pyenv_install_impl(repository_ctx):
py2 = repository_ctx.attr.py2_version
py3 = repository_ctx.attr.py3_version
py2 = repository_ctx.attr.py2
py3 = repository_ctx.attr.py3
py2_dir = repository_ctx.attr.py2_dir
py3_dir = repository_ctx.attr.py3_dir

_setup_pyenv(repository_ctx)
_install_python(repository_ctx, py2)
_install_python(repository_ctx, py3)
_setup_build_file(repository_ctx, py2, py3)
_setup_links(repository_ctx, py2, py2_dir)
_setup_links(repository_ctx, py3, py3_dir)
_setup_build_files(repository_ctx, py2, py3, py2_dir, py3_dir)

_pyenv_install = repository_rule(
_pyenv_install_impl,
attrs = {
"py2_version": attr.string(mandatory = True),
"py3_version": attr.string(mandatory = True),
"hermetic": attr.bool(mandatory = True),
# NOTE: Users may care about these private attributes at some point
"_pyenv_version": attr.string(
default = "1.2",
),
"_pyenv_repositories": attr.string_list_dict(
default = {
"1.2-unix": (
"https://github.com/pyenv/pyenv/archive/v1.2.18.tar.gz",
"pyenv-1.2.18",
"cc147f020178bb2f1ce0a8b9acb0bdf73979d967ce7d7415e22746e84e0eec7a",
),
"1.2-windows": (
"https://github.com/pyenv-win/pyenv-win/archive/v1.2.4.tar.gz",
"pyenv-win-1.2.4",
"0f3d3851eb692335c443a54700ffd99f0066f51c3b94ab0a867174c83f749554",
),
},
),
},
"py2": attr.string(mandatory = True, doc = "exact version of python2"),
"py3": attr.string(mandatory = True, doc = "exact version of python3"),
"py2_dir": attr.string(mandatory = True, doc = "directory for python2"),
"py3_dir": attr.string(mandatory = True, doc = "directory for python3"),
"hermetic": attr.bool(default = True, doc = "True if pyenv should be downloaded, False if local pyenv should be used"),
"pyenv_repo": attr.string_dict(default = {
"url": "https://github.com/pyenv/pyenv/archive/v1.2.18.tar.gz",
"strip_prefix": "pyenv-1.2.18",
"sha256": "cc147f020178bb2f1ce0a8b9acb0bdf73979d967ce7d7415e22746e84e0eec7a"
}, doc = "unix pyenv repository"),
"pyenv_win_repo": attr.string_dict(default = {
"url": "https://github.com/pyenv-win/pyenv-win/archive/v2.64.3.tar.gz",
"strip_prefix": "pyenv-win-2.64.3/pyenv-win",
"sha256": "9894fed264fb29e4aaff29728bea3c6f1c2d0106128fa8bbf17949f722d510d9"
}, doc = "windows pyenv repository")
}
)

def pyenv_install(py2, py3, hermetic = True):
def pyenv_install(py2, py3, py2_dir = PY2_DEFAULT_DIR_NAME, py3_dir = PY3_DEFAULT_DIR_NAME, name = DEFAULT_REPOSITORY_NAME, **kwargs):
"""
Macro to install and register a py_runtime_pair.
"""

maybe(
_pyenv_install,
name = "pyenv",
py2_version = py2,
py3_version = py3,
hermetic = hermetic,
name = name,
py2 = py2,
py3 = py3,
py2_dir = py2_dir,
py3_dir = py3_dir
)

native.register_toolchains("@pyenv//:python_toolchain")
native.register_toolchains("@{}//:python_toolchain".format(name))